diff --git a/.forgejo/workflows/gitleaks.yml b/.forgejo/workflows/gitleaks.yml new file mode 100644 index 0000000000..10d7847f33 --- /dev/null +++ b/.forgejo/workflows/gitleaks.yml @@ -0,0 +1,40 @@ +# .forgejo/workflows/gitleaks.yml +# +# Sulkta canonical gitleaks workflow. Drop a copy into every public repo at +# `.forgejo/workflows/gitleaks.yml` after the Forgejo act_runner is registered +# (task #295). +# +# Pairs with the pre-receive hook installed on every bare repo — that one is +# the strict enforcement layer (rejects the push); this one provides the +# per-PR red ✗ that branch-protection rules can require before merge. +# +# Layer 1 (this workflow): visible per-PR status, can be a required check. +# Layer 2 (pre-receive hook): strict enforcement at the server. +# Layer 3 (johnny5 cron sweep): nightly full-history sweep across all repos. + +name: gitleaks + +on: + push: + pull_request: + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # Full history — gitleaks needs depth to scan a commit range. + fetch-depth: 0 + + - name: install gitleaks + run: | + curl -sSL -o gl.tar.gz \ + https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz + tar xzf gl.tar.gz gitleaks + chmod +x gitleaks + ./gitleaks version + + - name: scan + run: | + ./gitleaks detect --source . --no-banner --redact --verbose diff --git a/.gitea/workflows/upstream-sync.yml b/.gitea/workflows/upstream-sync.yml new file mode 100644 index 0000000000..10487d3013 --- /dev/null +++ b/.gitea/workflows/upstream-sync.yml @@ -0,0 +1,116 @@ +name: Upstream sync + +# Daily check against the upstream mirror. Fast-forwards `main` to +# `upstream/develop` when upstream has advanced, then pings the Infra +# Matrix room so we know the wallet branch is due for a rebase. +# +# See SYNC.md on the wallet branch for the full topology + procedure +# this job implements. + +on: + schedule: + # 12:00 UTC daily — quiet time for all our time zones, avoids the + # morning-meeting window where an unexpected Matrix ping is noise. + - cron: '0 12 * * *' + workflow_dispatch: # manual trigger from the Actions UI too + +jobs: + sync-main: + runs-on: ubuntu-latest + env: + # The repo's .gitattributes (inherited from upstream) routes the + # screenshots/ tree through git-lfs. Gitea's LFS store doesn't hold + # those blobs, so on checkout the smudge filter tries to 404-download + # them and wedges git state for subsequent fetches. We don't need + # the image bytes here — leave LFS pointers as-is. + GIT_LFS_SKIP_SMUDGE: '1' + + steps: + - name: Checkout main + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + lfs: false + # Gitea's built-in GITEA_TOKEN is read-only by default. + # GIT_PUSH_TOKEN is a repo secret with a write-scoped PAT, so + # the subsequent `git push origin main` actually lands. + token: ${{ secrets.GIT_PUSH_TOKEN }} + + - name: Fetch upstream + wallet + run: | + set -euo pipefail + # Fetch directly from GitHub. We also have a Gitea pull-mirror + # at Sulkta-Coop/element-x-upstream that tracks this same repo, + # but sourcing from GitHub keeps the workflow independent of + # the mirror's health — one less moving part to diagnose. + git remote add upstream https://github.com/element-hq/element-x-android.git + git fetch --depth=500 upstream develop + git fetch origin wallet:refs/remotes/origin/wallet + + - name: Fast-forward main + id: ff + run: | + set -euo pipefail + git config user.name "sulkta-bot" + git config user.email "bot@sulkta.com" + # git-lfs pre-push hook refuses incomplete pushes — which triggers + # here because we skipped LFS smudge on checkout, so local LFS + # objects are absent. We're only pushing branch pointers (no new + # LFS content), so allow incomplete. + git config lfs.allowincompletepush true + OLD=$(git rev-parse --short HEAD) + echo "main was at $OLD" + if git merge --ff-only upstream/develop; then + NEW=$(git rev-parse --short HEAD) + if [ "$OLD" = "$NEW" ]; then + echo "main already up to date with upstream/develop" + echo "advanced=false" >> "$GITHUB_OUTPUT" + else + echo "main advanced: $OLD -> $NEW" + git push origin main + echo "advanced=true" >> "$GITHUB_OUTPUT" + echo "old=$OLD" >> "$GITHUB_OUTPUT" + echo "new=$NEW" >> "$GITHUB_OUTPUT" + fi + else + echo "::warning::main could not fast-forward to upstream/develop — someone committed to main directly?" + echo "advanced=false" >> "$GITHUB_OUTPUT" + fi + + - name: Measure wallet drift + if: steps.ff.outputs.advanced == 'true' + id: drift + run: | + set -euo pipefail + MB=$(git merge-base refs/remotes/origin/wallet main) + BEHIND=$(git rev-list --count "$MB..main") + NEW_ADDED=$(git rev-list --count "$MB..upstream/develop") + echo "behind=$BEHIND" >> "$GITHUB_OUTPUT" + echo "new_added=$NEW_ADDED" >> "$GITHUB_OUTPUT" + echo "wallet is $BEHIND commits behind main now; $NEW_ADDED new upstream commits this run" + + - name: Matrix notification (Infra room) + # Best-effort — if the target bot isn't in the room or Matrix is + # flapping, don't fail the whole run. The advance + push is the + # critical path; notify is a convenience ping. + if: steps.ff.outputs.advanced == 'true' + continue-on-error: true + env: + MATRIX_TOKEN: ${{ secrets.MATRIX_HOUSE_BOT_TOKEN }} + run: | + set -euo pipefail + TXN=$(date +%s%N) + ROOM='!rvxiUrWpgvMTAwzjGm:sulkta.com' # Infra + BODY="element-x upstream advanced · main ${{ steps.ff.outputs.old }} → ${{ steps.ff.outputs.new }} (${{ steps.drift.outputs.new_added }} commits). wallet is ${{ steps.drift.outputs.behind }} commits behind — rebase before next build." + + # jq keeps the body properly JSON-escaped; safer than shell interp + # shellcheck disable=SC2086 + PAYLOAD=$(printf '%s' "$BODY" | jq -Rs '{msgtype: "m.text", body: .}') + + curl --fail -s -X PUT \ + -H "Authorization: Bearer $MATRIX_TOKEN" \ + -H "Content-Type: application/json" \ + "https://chat.sulkta.com/_matrix/client/v3/rooms/${ROOM}/send/m.room.message/${TXN}" \ + -d "$PAYLOAD" + echo "notified" diff --git a/.github/renovate.json5 b/.github/renovate.json5 index c16310cf43..17e1fa0f1c 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -34,13 +34,6 @@ "/^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 deleted file mode 100644 index dd5524c7e3..0000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: APK Build - -on: - workflow_dispatch: - pull_request: - merge_group: - push: - branches: [ develop ] - -permissions: {} - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g - CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache - -jobs: - build: - name: Build APKs - runs-on: ubuntu-latest - permissions: - # For NejcZdovc/comment-pr - pull-requests: write - strategy: - matrix: - variant: [debug, release, nightly] - fail-fast: false - # Allow all jobs on develop. Just one per PR. - concurrency: - group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-{0}-{1}', matrix.variant, github.sha) || format('build-{0}-{1}', matrix.variant, github.ref) }} - cancel-in-progress: true - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Assemble debug APKs - if: ${{ matrix.variant == 'debug' }} - env: - ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} - ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} - ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }} - ELEMENT_SDK_SENTRY_DSN: ${{ secrets.ELEMENT_SDK_SENTRY_DSN }} - ELEMENT_CALL_SENTRY_DSN: ${{ secrets.ELEMENT_CALL_SENTRY_DSN }} - ELEMENT_CALL_POSTHOG_API_HOST: ${{ secrets.ELEMENT_CALL_POSTHOG_API_HOST }} - ELEMENT_CALL_POSTHOG_API_KEY: ${{ secrets.ELEMENT_CALL_POSTHOG_API_KEY }} - ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} - run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES - - name: Upload debug APKs - if: ${{ matrix.variant == 'debug' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: elementx-debug - 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 - id: diawi - # Do not fail the whole build if Diawi upload fails - continue-on-error: true - env: - token: ${{ secrets.DIAWI_TOKEN }} - if: ${{ matrix.variant == 'debug' && github.event_name == 'pull_request' && env.token != '' }} - with: - token: ${{ env.token }} - file: app/build/outputs/apk/gplay/debug/app-gplay-arm64-v8a-debug.apk - - name: Add or update PR comment with QR Code to download APK. - if: ${{ matrix.variant == 'debug' && github.event_name == 'pull_request' && steps.diawi.conclusion == 'success' }} - uses: NejcZdovc/comment-pr@a423635d183a8259308e80593c96fecf31539c26 # v2.1.0 - with: - message: | - :iphone: Scan the QR code below to install the build (arm64 only) for this PR. - ![QR code](${{ steps.diawi.outputs['qrcode'] }}) - If you can't scan the QR code you can install the build via this link: ${{ steps.diawi.outputs['url'] }} - # Enables to identify and update existing Ad-hoc release message on new commit in the PR - identifier: "GITHUB_COMMENT_QR_CODE" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Compile release sources - if: ${{ matrix.variant == 'release' }} - run: ./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES - - name: Compile nightly sources - if: ${{ matrix.variant == 'nightly' }} - run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml deleted file mode 100644 index 956655b262..0000000000 --- a/.github/workflows/build_enterprise.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Enterprise APK Build - -on: - workflow_dispatch: - pull_request: - merge_group: - push: - branches: [ develop ] - -permissions: {} - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g - CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache - -jobs: - build: - name: Build Enterprise APKs - runs-on: ubuntu-latest - # Skip in forks - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - strategy: - matrix: - variant: [debug, release, nightly] - fail-fast: false - # Allow all jobs on develop. Just one per PR. - concurrency: - group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-enterprise-{0}-{1}', matrix.variant, github.sha) || format('build-enterprise-{0}-{1}', matrix.variant, github.ref) }} - cancel-in-progress: true - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - run: git submodule update --init --recursive - - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Assemble debug Gplay Enterprise APK - if: ${{ matrix.variant == 'debug' }} - env: - ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} - ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} - ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }} - ELEMENT_SDK_SENTRY_DSN: ${{ secrets.ELEMENT_SDK_SENTRY_DSN }} - ELEMENT_CALL_SENTRY_DSN: ${{ secrets.ELEMENT_CALL_SENTRY_DSN }} - ELEMENT_CALL_POSTHOG_API_HOST: ${{ secrets.ELEMENT_CALL_POSTHOG_API_HOST }} - ELEMENT_CALL_POSTHOG_API_KEY: ${{ secrets.ELEMENT_CALL_POSTHOG_API_KEY }} - ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} - run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES - - name: Upload debug Enterprise APKs - if: ${{ matrix.variant == 'debug' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: elementx-enterprise-debug - path: | - app/build/outputs/apk/gplay/debug/*-universal-debug.apk - - name: Compile nightly and release sources - if: ${{ matrix.variant == 'release' }} - run: ./gradlew compileReleaseSources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES - - name: Compile nightly sources - if: ${{ matrix.variant == 'nightly' }} - run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml deleted file mode 100644 index 4bb51d05b5..0000000000 --- a/.github/workflows/danger.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Danger CI - -on: [pull_request, merge_group] - -permissions: {} - -jobs: - build: - runs-on: ubuntu-latest - name: Danger main check - # 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 - with: - persist-credentials: false - - name: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - 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 - - run: | - npm install --save-dev @babel/plugin-transform-flow-strip-types - - name: Danger - uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5 - with: - args: "--dangerfile ./tools/danger/dangerfile.js" - env: - DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} - # Fallback for forks - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/fork-pr-notice.yml b/.github/workflows/fork-pr-notice.yml deleted file mode 100644 index 3e67d97eac..0000000000 --- a/.github/workflows/fork-pr-notice.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Community PR notice - -on: - workflow_dispatch: - pull_request_target: # zizmor: ignore[dangerous-triggers] - types: - - opened - - reopened - -permissions: {} - -jobs: - welcome: - runs-on: ubuntu-latest - permissions: - # Require to comment the PR. - pull-requests: write - name: Welcome comment - # Only display it if base repo (upstream) is different from HEAD repo (possibly a fork) - if: github.event.pull_request.base.repo.full_name != github.event.pull_request.head.repo.full_name - steps: - - name: Add auto-generated commit warning - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `Thank you for your contribution! Here are a few things to check in the PR to ensure it's reviewed as quickly as possible: - - - If your pull request adds a feature or modifies the UI, this should have an equivalent pull request in the [Element X iOS repo](https://github.com/element-hq/element-x-ios) unless it only affects an Android-only behaviour or is behind a disabled feature flag, since we need parity in both clients to consider a feature done. It will also need to be approved by our product and design teams before being merged, so it's usually a good idea to discuss the changes in a Github issue first and then start working on them once the approach has been validated. - - Your branch should be based on \`origin/develop\`, at least when it was created. - - The title of the PR will be used for release notes, so it needs to describe the change visible to the user. - - The test pass locally running \`./gradlew test\`. - - The code quality check suite pass locally running \`./gradlew runQualityChecks\`. - - If you modified anything related to the UI, including previews, you'll have to run the \`Record screenshots\` GH action in your forked repo: that will generate compatible new screenshots. However, given Github Actions limitations, **it will prevent the CI from running temporarily**, until you upload a new commit after that one. To do so, just pull the latest changes and push [an empty commit](https://coderwall.com/p/vkdekq/git-commit-allow-empty).` - }) diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml deleted file mode 100644 index 8de21b3ef1..0000000000 --- a/.github/workflows/generate_github_pages.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Generate GitHub Pages -on: - workflow_dispatch: - schedule: - # At 00:00 on every Tuesday UTC - - cron: '0 0 * * 2' - -permissions: {} - -jobs: - generate-github-pages: - runs-on: ubuntu-latest - # Skip in forks - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - permissions: - contents: write - steps: - - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Set up Python 3.12 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: 3.14 - - name: Run World screenshots generation script - run: | - ./tools/test/generateWorldScreenshots.py - mkdir -p screenshots/en - cp tests/uitests/src/test/snapshots/images/* screenshots/en - - name: Deploy GitHub Pages - 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 deleted file mode 100644 index 66078b7b4b..0000000000 --- a/.github/workflows/gradle-wrapper-update.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Update Gradle Wrapper - -on: - workflow_dispatch: - schedule: - - cron: "0 0 * * *" - -permissions: {} - -jobs: - update-gradle-wrapper: - runs-on: ubuntu-latest - # Skip in forks - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - name: Use JDK 21 - if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch' - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Update Gradle Wrapper - uses: gradle-update/update-gradle-wrapper-action@512b1875f3b6270828abfe77b247d5895a2da1e5 # v2.1.0 - with: - repo-token: ${{ secrets.DANGER_GITHUB_API_TOKEN }} - target-branch: develop - labels: PR-Build diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml deleted file mode 100644 index 9af5c6bc1d..0000000000 --- a/.github/workflows/maestro-local.yml +++ /dev/null @@ -1,149 +0,0 @@ -name: Maestro (local) - -# Run this flow only when APK Build workflow completes -on: - workflow_dispatch: - pull_request: - -permissions: {} - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g - CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache - ARCH: x86_64 - DEVICE: pixel_7_pro - API_LEVEL: 33 - TARGET: google_apis - -jobs: - build-apk: - name: Build APK - runs-on: ubuntu-latest - concurrency: - group: ${{ format('maestro-build-{0}', github.ref) }} - cancel-in-progress: true - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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 - 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 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Assemble debug APK - run: ./gradlew :app:assembleGplayDebug $CI_GRADLE_ARG_PROPERTIES - env: - ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} - ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} - ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - - name: Upload APK as artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: elementx-apk-maestro - path: | - app/build/outputs/apk/gplay/debug/app-gplay-x86_64-debug.apk - retention-days: 5 - overwrite: true - if-no-files-found: error - - maestro-cloud: - name: Maestro test suite - runs-on: ubuntu-latest - needs: [ build-apk ] - # Allow only one to run at a time, since they use the same environment. - # Otherwise, tests running in parallel can break each other. - concurrency: - group: maestro-test - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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 - # https://github.com/actions/checkout/issues/881 - ref: ${{ github.ref }} - persist-credentials: false - - name: Download APK artifact from previous job - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: elementx-apk-maestro - - name: Enable KVM group perms - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: Install maestro - run: curl -fsSL "https://get.maestro.mobile.dev" | bash - - name: Run Maestro tests in emulator - id: maestro_test - uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 - continue-on-error: true - env: - MAESTRO_USERNAME: maestroelement - MAESTRO_PASSWORD: ${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }} - MAESTRO_RECOVERY_KEY: ${{ secrets.MATRIX_MAESTRO_ACCOUNT_RECOVERY_KEY }} - MAESTRO_ROOM_NAME: MyRoom - MAESTRO_INVITEE1_MXID: "@maestroelement2:matrix.org" - MAESTRO_INVITEE2_MXID: "@maestroelement3:matrix.org" - MAESTRO_APP_ID: io.element.android.x.debug - with: - api-level: ${{ env.API_LEVEL }} - arch: ${{ env.ARCH }} - profile: ${{ env.DEVICE }} - target: ${{ env.TARGET }} - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - disk-size: 3G - script: | - .github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk - - name: Upload test results - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: test-results - path: | - ~/.maestro/tests/** - retention-days: 5 - overwrite: true - if-no-files-found: error - - name: Update summary (success) - if: steps.maestro_test.outcome == 'success' - run: | - echo "### Maestro tests worked :rocket:!" >> $GITHUB_STEP_SUMMARY - - name: Update summary (failure) - if: steps.maestro_test.outcome != 'success' - run: | - LOG_FILE=$(find ~/.maestro/tests/ -name maestro.log) - echo "Log file: $LOG_FILE" - LOG_LINES="$(tail -n 30 $LOG_FILE)" - echo "### :x: Maestro tests failed... - - \`\`\` - $LOG_LINES - \`\`\`" >> $GITHUB_STEP_SUMMARY - - name: Fail the workflow in case of error in test - if: steps.maestro_test.outcome != 'success' - run: | - echo "Maestro tests failed. Please check the logs." - exit 1 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index 31a8806a85..0000000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Build and release nightly application - -on: - workflow_dispatch: - schedule: - # Every nights at 4 - - cron: "0 4 * * *" - -permissions: {} - -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g - CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache - -jobs: - nightly: - name: Build and publish nightly bundle to Firebase - runs-on: ubuntu-latest - if: ${{ github.repository == 'element-hq/element-x-android' }} - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Build and upload Nightly application - run: | - ./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES - env: - ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} - ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} - ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }} - ELEMENT_SDK_SENTRY_DSN: ${{ secrets.ELEMENT_SDK_SENTRY_DSN }} - ELEMENT_CALL_SENTRY_DSN: ${{ secrets.ELEMENT_CALL_SENTRY_DSN }} - ELEMENT_CALL_POSTHOG_API_HOST: ${{ secrets.ELEMENT_CALL_POSTHOG_API_HOST }} - ELEMENT_CALL_POSTHOG_API_KEY: ${{ secrets.ELEMENT_CALL_POSTHOG_API_KEY }} - ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} - ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }} - ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }} - ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }} - FIREBASE_TOKEN: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_FIREBASE_TOKEN }} - - name: Additionally upload Nightly APK to browserstack for testing - continue-on-error: true # don't block anything by this upload failing (for now) - run: | - curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_PASSWORD" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@app/build/outputs/apk/gplay/nightly/app-gplay-universal-nightly.apk" -F "custom_id=element-x-android-nightly" - env: - BROWSERSTACK_USERNAME: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_USERNAME }} - BROWSERSTACK_PASSWORD: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_ACCESS_KEY }} diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml deleted file mode 100644 index 371c11b3ff..0000000000 --- a/.github/workflows/nightlyReports.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Nightly reports - -on: - workflow_dispatch: - schedule: - # Every nights at 5 - - cron: "0 5 * * *" - -permissions: {} - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g - CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache - -jobs: - nightlyReports: - name: Create kover report artifact and upload sonar result. - runs-on: ubuntu-latest - if: ${{ github.repository == 'element-hq/element-x-android' }} - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - - 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 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: false - - - name: ⚙️ Run unit tests, debug and release - run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES - - - name: 📸 Run screenshot tests - run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES - - - name: 📈 Generate kover report and verify coverage - run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES - - - name: ✅ Upload kover report - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: kover-results - path: | - **/build/reports/kover - - - name: 🔊 Publish results to Sonar - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} - if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} - run: ./gradlew assembleDebug createFullJarDebugTestFixtures :app:createFullJarGplayDebugTestFixtures $CI_GRADLE_ARG_PROPERTIES - - # Gradle dependency analysis using https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin - dependency-analysis: - name: Dependency analysis - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Dependency analysis - run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES - - name: Upload dependency analysis - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: dependency-analysis - path: build/reports/dependency-check-report.html diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml deleted file mode 100644 index 6efeff17be..0000000000 --- a/.github/workflows/post-release.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Post-release - -on: - push: - tags: - - 'v*' - -permissions: {} - -jobs: - post-release: - runs-on: ubuntu-latest - # Skip in forks - if: github.repository == 'element-hq/element-x-android' - - steps: - - name: Trigger pipeline - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.ENTERPRISE_ACTIONS_TOKEN }} - script: | - const tag = context.ref.replace('refs/tags/', ''); - const inputs = { git_tag: tag }; - await github.rest.actions.createWorkflowDispatch({ - owner: 'element-hq', - repo: 'element-enterprise', - workflow_id: 'pipeline-android.yml', - ref: 'main', - inputs: inputs - }); diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml deleted file mode 100644 index f0c8fd1e6f..0000000000 --- a/.github/workflows/pull_request.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Pull Request -on: - pull_request_target: - types: [ opened, edited, labeled, unlabeled, synchronize ] - workflow_call: # zizmor: ignore[dangerous-triggers] - secrets: - ELEMENT_BOT_TOKEN: - required: true - -permissions: {} - -jobs: - prevent-blocked: - name: Prevent blocked - runs-on: ubuntu-latest - permissions: - pull-requests: read - steps: - - name: Add notice - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - if: contains(github.event.pull_request.labels.*.name, 'X-Blocked') - with: - script: | - core.setFailed("PR has been labeled with X-Blocked; it cannot be merged."); - - community-prs: - name: Label Community PRs - runs-on: ubuntu-latest - if: github.event.action == 'opened' - permissions: - pull-requests: write - steps: - - name: Check membership - if: github.event.pull_request.user.login != 'renovate[bot]' - uses: tspascoal/get-user-teams-membership@818140d631d5f29f26b151afbe4179f87d9ceb5e # v4.0.1 - id: teams - with: - username: ${{ github.event.pull_request.user.login }} - organization: element-hq - team: Vector Core - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }} - - name: Add label - if: steps.teams.outputs.isTeamMember == 'false' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['Z-Community-PR'] - }); - - close-if-fork-develop: - name: Forbid develop branch fork contributions - runs-on: ubuntu-latest - permissions: - # Require to comment and close the PR. - pull-requests: write - if: > - github.event.action == 'opened' && - github.event.pull_request.head.ref == 'develop' && - github.event.pull_request.head.repo.full_name != github.repository - steps: - - name: Close pull request - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" + - " branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity.", - }); - - github.rest.pulls.update({ - pull_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - state: 'closed' - }); diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml deleted file mode 100644 index ceaa86016a..0000000000 --- a/.github/workflows/quality.yml +++ /dev/null @@ -1,369 +0,0 @@ -name: Code Quality Checks - -on: - workflow_dispatch: - pull_request: - merge_group: - push: - branches: [ main, develop ] - -permissions: {} - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g - CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache - -jobs: - checkScript: - name: Search for forbidden patterns - runs-on: ubuntu-latest - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - 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: Run code quality check suite - run: ./tools/check/check_code_quality.sh - - checkScreenshot: - name: Search for invalid screenshot files - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Set up Python 3.12 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: 3.14 - - name: Search for invalid screenshot files - run: ./tools/test/checkInvalidScreenshots.py - - checkDependencies: - name: Search for invalid dependencies - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Set up Python 3.12 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: 3.14 - - name: Search for invalid dependencies - run: ./tools/dependencies/checkDependencies.py - - # Code checks - konsist: - name: Konsist tests - runs-on: ubuntu-latest - # Allow all jobs on main and develop. Just one per PR. - concurrency: - 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 - 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: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - 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 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Run Konsist tests - run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon - - name: Upload reports - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: konsist-report - path: | - **/build/reports/**/*.* - - compose: - name: Compose tests - runs-on: ubuntu-latest - # Allow all jobs on main and develop. Just one per PR. - concurrency: - 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 - 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: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - 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 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Run compose tests - run: ./tools/compose/check_stability.sh - - lint: - name: Android lint check - runs-on: ubuntu-latest - # Allow all jobs on main and develop. Just one per PR. - concurrency: - 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 - 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: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - 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 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Build Gplay Debug - run: ./gradlew :app:compileGplayDebugKotlin $CI_GRADLE_ARG_PROPERTIES - - name: Build Fdroid Debug - run: ./gradlew :app:compileFdroidDebugKotlin $CI_GRADLE_ARG_PROPERTIES - - name: Run lint - run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue - - name: Upload reports - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: linting-report - path: | - **/build/reports/**/*.* - - detekt: - name: Detekt checks - runs-on: ubuntu-latest - # Allow all jobs on main and develop. Just one per PR. - concurrency: - 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 - 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: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - 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 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Run Detekt - run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon - - name: Upload reports - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: detekt-report - path: | - **/build/reports/**/*.* - - ktlint: - name: Ktlint checks - runs-on: ubuntu-latest - # Allow all jobs on main and develop. Just one per PR. - concurrency: - 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 - 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: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - 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 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Run Ktlint check - run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES - - name: Upload reports - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ktlint-report - path: | - **/build/reports/**/*.* - - docs: - name: Doc checks - runs-on: ubuntu-latest - # Allow all jobs on main and develop. Just one per PR. - concurrency: - 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 - 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: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - 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: Run docs check - # This is equivalent to `./gradlew checkDocs`, but we avoid having to install java and gradle - run: python3 ./tools/docs/generate_toc.py --verify ./*.md docs/**/*.md - - # Note: to auto fix issues you can use the following command: - # shellcheck -f diff | git apply - shellcheck: - name: Check shell scripts - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Run shellcheck - uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0 - with: - severity: warning - - zizmor: - name: Run zizmor - runs-on: ubuntu-latest - permissions: - security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 - - upload_reports: - name: Project Check Suite - runs-on: ubuntu-latest - 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 - 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: Download reports from previous jobs - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - - name: Prepare Danger - if: always() - run: | - npm install --save-dev @babel/core - npm install --save-dev @babel/plugin-transform-flow-strip-types - yarn add danger-plugin-lint-report --dev - - name: Danger lint - if: always() - uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5 - with: - args: "--dangerfile ./tools/danger/dangerfile-lint.js" - env: - DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} - # Fallback for forks - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml deleted file mode 100644 index 4b70cffe61..0000000000 --- a/.github/workflows/recordScreenshots.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Record screenshots - -on: - workflow_dispatch: - pull_request: - types: [ labeled ] - -permissions: {} - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dsonar.gradle.skipCompile=true - CI_GRADLE_ARG_PROPERTIES: --no-configuration-cache - -jobs: - record: - 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' - - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - - name: Remove Record-Screenshots label - if: github.event.label.name == 'Record-Screenshots' - uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 - with: - labels: Record-Screenshots - - name: ⏬ Checkout with LFS (PR) - if: github.event.label.name == 'Record-Screenshots' - uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - with: - persist-credentials: false - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} - - name: ⏬ Checkout with LFS (Branch) - if: github.event_name == 'workflow_dispatch' - uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - with: - persist-credentials: false - - name: ☕️ Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.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 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Record screenshots - id: record - run: ./.github/workflows/scripts/recordScreenshots.sh - env: - GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }} - GRADLE_ARGS: ${{ env.CI_GRADLE_ARG_PROPERTIES }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index eece5ab0d4..0000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,146 +0,0 @@ -name: Create release App Bundle and APKs - -on: - workflow_dispatch: - push: - branches: [ main ] - -permissions: {} - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g - CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache - -jobs: - gplay: - name: Create App Bundle (Gplay) - runs-on: ubuntu-latest - concurrency: - group: ${{ format('build-release-main-gplay-{0}', github.sha) }} - cancel-in-progress: true - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - - name: Create app bundle - env: - ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} - ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} - ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }} - ELEMENT_SDK_SENTRY_DSN: ${{ secrets.ELEMENT_SDK_SENTRY_DSN }} - ELEMENT_CALL_SENTRY_DSN: ${{ secrets.ELEMENT_CALL_SENTRY_DSN }} - ELEMENT_CALL_POSTHOG_API_HOST: ${{ secrets.ELEMENT_CALL_POSTHOG_API_HOST }} - ELEMENT_CALL_POSTHOG_API_KEY: ${{ secrets.ELEMENT_CALL_POSTHOG_API_KEY }} - ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} - run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES - - name: Upload bundle as artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: elementx-app-gplay-bundle-unsigned - path: | - app/build/outputs/bundle/gplayRelease/app-gplay-release.aab - - enterprise: - name: Create App Bundle Enterprise - runs-on: ubuntu-latest - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - concurrency: - group: ${{ format('build-release-main-enterprise-{0}', github.sha) }} - cancel-in-progress: true - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - run: git submodule update --init --recursive - - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - - name: Create Enterprise app bundle - env: - ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} - ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} - ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES - - name: Upload bundle as artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: elementx-enterprise-app-gplay-bundle-unsigned - path: | - app/build/outputs/bundle/gplayRelease/app-gplay-release.aab - - fdroid: - name: Create APKs (FDroid) - runs-on: ubuntu-latest - concurrency: - group: ${{ format('build-release-main-fdroid-{0}', github.sha) }} - cancel-in-progress: true - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - - name: Create APKs - env: - ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} - ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} - ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES - - name: Upload apks as artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: elementx-app-fdroid-apks-unsigned - path: | - app/build/outputs/apk/fdroid/release/*.apk diff --git a/.github/workflows/scripts/maestro/local-recording.sh b/.github/workflows/scripts/maestro/local-recording.sh deleted file mode 100755 index adc83f4876..0000000000 --- a/.github/workflows/scripts/maestro/local-recording.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -# -# Copyright (c) 2025 Element Creations Ltd. -# Copyright 2024 New Vector Ltd. -# -# SPDX-License-Identifier: AGPL-3.0-only. -# Please see LICENSE in the repository root for full details. -# - -COUNT=0 -mkdir -p /data/local/tmp/recordings; -FILENAME=/data/local/tmp/recordings/testRecording$COUNT.mp4 -while true - do - COUNT=$((COUNT+1)) - FILENAME=/data/local/tmp/recordings/testRecording$COUNT.mp4 - printf "\nRecording video file #%d\n" $COUNT - screenrecord --bugreport --bit-rate=16m --size 720x1280 $FILENAME - done diff --git a/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh b/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh deleted file mode 100755 index 4ee021c316..0000000000 --- a/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/sh - -# -# Copyright (c) 2025 Element Creations Ltd. -# Copyright 2024 New Vector Ltd. -# -# SPDX-License-Identifier: AGPL-3.0-only. -# Please see LICENSE in the repository root for full details. -# - -# First we disable the onboarding flow on Chrome, which is a source of issues -# (see https://stackoverflow.com/a/64629745) -echo "Disabling Chrome onboarding flow" -adb shell am set-debug-app --persistent com.android.chrome -adb shell 'echo "chrome --disable-fre --no-default-browser-check --no-first-run" > /data/local/tmp/chrome-command-line' -adb shell am start -n com.android.chrome/com.google.android.apps.chrome.Main - -adb install -r $1 -echo "Starting the screen recording..." -adb push .github/workflows/scripts/maestro/local-recording.sh /data/local/tmp/ -adb shell "chmod +x /data/local/tmp/local-recording.sh" -mkdir -p ~/.maestro/tests -# Start logcat in the background and save the output to a file, use `org.matrix.rust.sdk` tag since the SDK handles the logging -adb logcat 'org.matrix.rust.sdk:D *:S' > ~/.maestro/tests/logcat.txt & -adb shell "/data/local/tmp/local-recording.sh & echo \$! > /data/local/tmp/screenrecord_pid.txt" & -set +e -~/.maestro/bin/maestro test .maestro/allTests.yaml -TEST_STATUS=$? -echo "Test run completed with status $TEST_STATUS" - -# Stop the screen recording loop -SCRIPT_PID=$(adb shell "cat /data/local/tmp/screenrecord_pid.txt") -adb shell "kill -2 $SCRIPT_PID" - -# Get the PID of the screen recording process -SCREENRECORD_PID=$(adb shell ps | grep screenrecord | awk '{print $2}') -# Wait for the screen recording process to exit -while [ ! -z $SCREENRECORD_PID ]; do - echo "Waiting for screen recording ($SCREENRECORD_PID) to finish..." - adb shell "kill -2 $SCREENRECORD_PID" - sleep 1 - SCREENRECORD_PID=$(adb shell ps | grep screenrecord | awk '{print $2}') -done - -adb pull /data/local/tmp/recordings/ ~/.maestro/tests/ -exit $TEST_STATUS diff --git a/.github/workflows/scripts/parse_test_failures.py b/.github/workflows/scripts/parse_test_failures.py deleted file mode 100644 index eb0a0ecafa..0000000000 --- a/.github/workflows/scripts/parse_test_failures.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -import xml.etree.ElementTree as ET -import sys -import glob - -screenshot_test_failures = [] -output = [] - -def parse_test_failures(xml_file): - """Parse XML test results and print failures.""" - tree = ET.parse(xml_file) - root = tree.getroot() - - # Find all testcase elements with failure children - if root.get("failures", "0") == "0": - return - - name = root.get('name', 'Test Suite') - is_screenshot_test = name.startswith('ui.Preview') - - if not is_screenshot_test: - output.append(f"## {name}") - - for testcase in root.findall('.//testcase'): - failure = testcase.find('failure') - if failure is not None: - # Get testcase attributes - classname = testcase.get('classname', '') - name = testcase.get('name', '') - - if is_screenshot_test: - # For screenshot tests, we want to display the classname as well - screenshot_test_failures.append(f"{classname}.{name}") - else: - # Get failure content (text inside the failure element) - failure_message = failure.get('message', '') - failure_content = failure.text if failure.text else '' - - # Print in the requested format - output.append(f"### {name}") - output.append("```") - output.append(failure_message) - output.append("```") - output.append("
Stacktrace") - output.append(f"
{failure_content}
") - output.append("
") - output.append("\n") - -if __name__ == "__main__": - if len(sys.argv) < 2: - output.append("Usage: parse_test_failures.py ", file=sys.stderr) - sys.exit(1) - - file = sys.argv[1] - - if file.endswith('xml'): - parse_test_failures(file) - else: - files = glob.glob("**/build/test-results/*UnitTest/*.xml", root_dir = file, recursive = True) - for file in files: - parse_test_failures(file) - - if screenshot_test_failures: - output.append("## Screenshot Test Failures") - output.append("```") - for failure in screenshot_test_failures: - output.append(failure) - output.append("```") - - text_output = '\n'.join(output) - # Trim output larger than 1MB to avoid GitHub Action log limits - while len(text_output.encode('utf-8')) > 1_040_000: - output.pop(-2) - output.append("## !!! Truncated output due to size limits. !!!") - text_output = '\n'.join(output) - - print(text_output) diff --git a/.github/workflows/scripts/recordScreenshots.sh b/.github/workflows/scripts/recordScreenshots.sh deleted file mode 100755 index d29353a2c2..0000000000 --- a/.github/workflows/scripts/recordScreenshots.sh +++ /dev/null @@ -1,90 +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. - -set -e - -TOKEN=$GITHUB_TOKEN -REPO=$GITHUB_REPOSITORY - -SHORT=t:,r: -LONG=token:,repo: -OPTS=$(getopt -a -n recordScreenshots --options $SHORT --longoptions $LONG -- "$@") - -eval set -- "$OPTS" -while : -do - case "$1" in - -t | --token ) - TOKEN="$2" - shift 2 - ;; - -r | --repo ) - REPO="$2" - shift 2 - ;; - --) - shift; - break - ;; - *) - echo "Unexpected option: $1" - help - ;; - esac -done - -BRANCH=$(git rev-parse --abbrev-ref HEAD) -echo Branch used: $BRANCH - -if [[ -z ${TOKEN} ]]; then - echo "No token specified, either set the env var GITHUB_TOKEN or use the --token option" - exit 1 -fi - -if [[ -z ${REPO} ]]; then - echo "No repo specified, either set the env var GITHUB_REPOSITORY or use the --repo option" - exit 1 -fi - -echo "Deleting previous screenshots" -./gradlew removeOldSnapshots --stacktrace --warn $GRADLE_ARGS - -echo "Record screenshots" -./gradlew recordPaparazziDebug --stacktrace $GRADLE_ARGS - -echo "Deleting previous screenshots" -./gradlew removeOldScreenshots --stacktrace --warn $GRADLE_ARGS - -echo "Record screenshots (Compound)" -./gradlew :libraries:compound:recordRoborazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn $GRADLE_ARGS - -echo "Committing changes" -git config http.sslVerify false - -if [[ -z ${INPUT_AUTHOR_NAME} ]]; then - git config user.name "ElementBot" -else - git config --local user.name "${INPUT_AUTHOR_NAME}" -fi - -if [[ -z ${INPUT_AUTHOR_EMAIL} ]]; then - git config user.email "android@element.io" -else - git config --local user.name "${INPUT_AUTHOR_EMAIL}" -fi -git add -A -git commit -m "Update screenshots" - -GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git" -echo "Pushing changes" -if [[ -z ${GITHUB_ACTOR} ]]; then - echo "No GITHUB_ACTOR env var" - GITHUB_REPO="https://$TOKEN@github.com/$REPO.git" -fi -git push $GITHUB_REPO "$BRANCH" -echo "Done!" diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml deleted file mode 100644 index 40cf9d0058..0000000000 --- a/.github/workflows/sonar.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Sonar - -on: - workflow_dispatch: - pull_request: - merge_group: - push: - branches: [ main, develop ] - -permissions: {} - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g - CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true --no-configuration-cache - GROUP: ${{ format('sonar-{0}', github.ref) }} - -jobs: - sonar: - name: Sonar Quality Checks - runs-on: ubuntu-latest - # Allow all jobs on main and develop. Just one per PR. - concurrency: - group: ${{ format('sonar-{0}', github.ref) }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Build debug code and test fixtures - run: ./gradlew assembleGplayDebug createFullJarDebugTestFixtures :app:createFullJarGplayDebugTestFixtures $CI_GRADLE_ARG_PROPERTIES - - name: 🔊 Publish results to Sonar - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} - if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} - run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml deleted file mode 100644 index 1958e80083..0000000000 --- a/.github/workflows/stale-issues.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Close stale issues that are missing info. - -on: - schedule: - - cron: "30 1 * * *" - -permissions: {} - -jobs: - stale: - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 - with: - only-labels: "X-Needs-Info" - days-before-issue-stale: 30 - days-before-issue-close: 7 - days-before-pr-stale: -1 - stale-issue-label: "stale" - labels-to-remove-when-unstale: "X-Needs-Info" - stale-issue-message: "This issue has been awaiting further information for the past 30 days so will now be marked as stale. Please provide the requested information within the next 7 days to keep it open." - close-issue-message: "This issue is being closed due to inactivity after further information was requested." diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml deleted file mode 100644 index 6de0f3b0df..0000000000 --- a/.github/workflows/sync-localazy.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Sync Localazy -on: - workflow_dispatch: - schedule: - # At 00:00 on every Monday UTC - - cron: '0 0 * * 1' - -permissions: {} - -jobs: - sync-localazy: - runs-on: ubuntu-latest - # Skip in forks - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - name: Set up Python 3.12 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: 3.14 - - name: Setup Localazy - run: | - curl -sS https://dist.localazy.com/debian/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/localazy.gpg - echo "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/localazy.gpg] https://maven.localazy.com/repository/apt/ stable main" | sudo tee /etc/apt/sources.list.d/localazy.list - sudo apt-get update && sudo apt-get install localazy - - name: Run Localazy script - run: | - ./tools/localazy/downloadStrings.sh --all - ./tools/localazy/importSupportedLocalesFromLocalazy.py - ./tools/test/generateAllScreenshots.py - - name: Create Pull Request for Strings - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - token: ${{ secrets.DANGER_GITHUB_API_TOKEN }} - commit-message: Sync Strings from Localazy - title: Sync Strings - body: | - - Update Strings from Localazy - branch: sync-localazy - base: develop - labels: PR-i18n diff --git a/.github/workflows/sync-sas-strings.yml b/.github/workflows/sync-sas-strings.yml deleted file mode 100644 index 9f7a67cc22..0000000000 --- a/.github/workflows/sync-sas-strings.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Sync SAS strings -on: - workflow_dispatch: - schedule: - # At 00:00 on every Monday UTC - - cron: '0 0 * * 1' - -permissions: {} - -jobs: - sync-sas-strings: - runs-on: ubuntu-latest - # Skip in forks - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - # No concurrency required, runs every time on a schedule. - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Set up Python 3.12 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: 3.14 - - name: Install Prerequisite dependencies - run: | - pip install requests - - name: Run SAS String script - run: ./tools/sas/import_sas_strings.py - - name: Create Pull Request for SAS Strings - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - commit-message: Sync SAS Strings - title: Sync SAS Strings - body: | - - Update SAS Strings from matrix-doc. - branch: sync-sas-strings - base: develop - labels: PR-Misc - - diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 842c8113f4..0000000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: Test - -on: - workflow_dispatch: - pull_request: - merge_group: - push: - branches: [ main, develop ] - -permissions: {} - -# Enrich gradle.properties for CI/CD -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC - CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache - -jobs: - tests: - name: Runs unit tests - runs-on: ubuntu-latest - - # Allow all jobs on main and develop. Just one per PR. - concurrency: - group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }} - cancel-in-progress: true - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be - with: - # This might remove tools that are actually needed, if set to "true" but frees about 6 GB - tool-cache: true - # All of these default to true, but we should only need the 'android' one (and maybe swap-storage?) - android: false - dotnet: true - haskell: true - # This takes way too long to run (~2 minutes) and it saves only ~5.5GB - large-packages: false - docker-images: true - swap-storage: false - - # Increase swapfile size to prevent screenshot tests getting terminated - # https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749 - - name: 💽 Increase swapfile size - run: | - sudo swapoff -a - sudo fallocate -l 8G /mnt/swapfile - sudo chmod 600 /mnt/swapfile - sudo mkswap /mnt/swapfile - sudo swapon /mnt/swapfile - sudo swapon --show - - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - with: - # Ensure we are building the branch and not the branch after being merged on develop - # https://github.com/actions/checkout/issues/881 - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - - name: Add SSH private keys for submodule repositories - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} - with: - ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }} - - name: Clone submodules - 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 - with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '21' - - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - with: - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - - - name: ⚙️ Check coverage for debug variant (includes unit & screenshot tests) - run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :koverXmlReportMerged :koverHtmlReportMerged :koverVerifyAll $CI_GRADLE_ARG_PROPERTIES - - - name: 🚫 Upload kover failed coverage reports - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: kover-error-report - path: | - app/build/reports/kover - - - name: ✅ Upload kover report (disabled) - if: always() - run: echo "This is now done only once a day, see nightlyReports.yml" - - - name: 🚫 Upload test results on error - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: tests-and-screenshot-tests-results - path: | - **/build/paparazzi/failures/ - **/build/roborazzi/failures/ - **/build/reports/tests/*UnitTest/ - - - name: 🚫 Modify summary on error - if: failure() - run: | - echo """## Tests failed! - - """ >> $GITHUB_STEP_SUMMARY - python3 .github/workflows/scripts/parse_test_failures.py . >> $GITHUB_STEP_SUMMARY - echo "---" >> $GITHUB_STEP_SUMMARY - - # https://github.com/codecov/codecov-action - - name: ☂️ Upload coverage reports to codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 - with: - fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} - files: build/reports/kover/reportMerged.xml - verbose: true diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml deleted file mode 100644 index b93ea81403..0000000000 --- a/.github/workflows/triage-incoming.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Move new issues onto issue triage board v2 - -on: - issues: - types: [ opened ] - -permissions: {} - -jobs: - triage-new-issues: - runs-on: ubuntu-latest - steps: - - 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 deleted file mode 100644 index 0b587369ed..0000000000 --- a/.github/workflows/triage-labelled.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Move labelled issues to correct boards and columns - -on: - issues: - types: [labeled] - -permissions: {} - -jobs: - move_element_x_issues: - name: ElementX issues to ElementX project board - runs-on: ubuntu-latest - # Skip in forks - if: > - github.repository == 'element-hq/element-x-android' - steps: - - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 - with: - project-url: https://github.com/orgs/element-hq/projects/43 - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - - move_needs_info: - name: Move triaged needs info issues on board - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 - id: addItem - with: - project-url: https://github.com/orgs/element-hq/projects/91 - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - labeled: X-Needs-Info - - name: Print itemId - run: echo ${STEPS_ADDITEM_OUTPUTS_ITEMID} - env: - STEPS_ADDITEM_OUTPUTS_ITEMID: ${{ steps.addItem.outputs.itemId }} - - uses: kalgurn/update-project-item-status@31e54df46a2cdaef4f85c31ac839fbcd2fd7c3a2 # 0.0.3 - if: ${{ steps.addItem.outputs.itemId }} - with: - project-url: https://github.com/orgs/element-hq/projects/91 - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - item-id: ${{ steps.addItem.outputs.itemId }} - status: "Needs info" - - ex_plorers: - name: Add labelled issues to X-Plorer project - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'Team: Element X Feature') - steps: - - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 - with: - project-url: https://github.com/orgs/element-hq/projects/73 - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - - verticals_feature: - name: Add labelled issues to Verticals Feature project - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'Team: Verticals Feature') - steps: - - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 - with: - project-url: https://github.com/orgs/element-hq/projects/57 - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - - qa: - name: Add labelled issues to QA project - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'Team: QA') || - contains(github.event.issue.labels.*.name, 'X-Needs-Signoff') - steps: - - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 - with: - project-url: https://github.com/orgs/element-hq/projects/69 - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - - signoff: - name: Add labelled issues to signoff project - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'X-Needs-Signoff') - steps: - - 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/.github/workflows/validate-lfs.yml b/.github/workflows/validate-lfs.yml deleted file mode 100644 index 027c7d68e9..0000000000 --- a/.github/workflows/validate-lfs.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Validate Git LFS - -on: [pull_request, merge_group] - -permissions: {} - -jobs: - build: - runs-on: ubuntu-latest - name: Validate - steps: - - uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - - - run: | - ./tools/git/validate_lfs.sh diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000000..11864819e5 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,38 @@ +# gitleaks config — element-x-ada +# +# Element X is a Matrix client fork with Cardano ADA integration. +# Patterns flagged are all public-by-design or doc/test fixtures: +# - PostHog apiKey: client-side analytics token, public on every PostHog- +# integrated mobile app. Identifies the project, doesn't grant write. +# - MapTiler API_KEY: client-side maps token, ships in every release +# - google-services.json: Firebase config — Google explicitly documents +# this as public-by-design (all real auth goes through FirebaseAuth) +# - Segment readKey: client-side write key +# - user_signing_key in KDoc comments: example values in doc-strings +# - docs/ + *Test.kt files: scratch + test fixtures, never live credentials + +[extend] +useDefault = true + +[allowlist] +description = "Public client keys (PostHog, MapTiler, Firebase, Segment) + docs + test fixtures" +paths = [ + '''docs/.*''', + '''.*/google-services\.json''', + '''.*Test\.kt''', + '''localazy\.json''', + '''tools/localazy/.*''', +] +regexTarget = "line" +regexes = [ + # PostHog client keys — match any variable name ending in apiKey + '''[a-zA-Z]*[Aa]piKey\s*=\s*"phc_[A-Za-z0-9_-]{20,}"''', + # MapTiler / similar public client keys named API_KEY constant + '''const\s+val\s+API_KEY\s*=\s*"''', + # Segment write keys (Kotlin style) + '''readKey\s*=\s*"''', + # Localazy / Segment readKey (JSON style) + '''"readKey"\s*:\s*"''', + # Matrix protocol KDoc examples (* prefix is the KDoc comment shape) + '''^\s*\*\s*"user_signing_key"\s*:\s*"''', +] diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index f393d5cdd1..76f6344777 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,7 +1,6 @@ - diff --git a/.maestro/tests/roomList/createAndDeleteDM.yaml b/.maestro/tests/roomList/createAndDeleteDM.yaml index eed576c04f..a6279151ea 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: "Continue" +- tapOn: "Send invite" - takeScreenshot: build/maestro/330-createAndDeleteDM - tapOn: "maestroelement2" - scroll diff --git a/.maestro/tests/roomList/createAndDeleteRoom.yaml b/.maestro/tests/roomList/createAndDeleteRoom.yaml index adf9d7cf29..b53066ccd5 100644 --- a/.maestro/tests/roomList/createAndDeleteRoom.yaml +++ b/.maestro/tests/roomList/createAndDeleteRoom.yaml @@ -24,16 +24,8 @@ 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/BLOCKERS.md b/BLOCKERS.md new file mode 100644 index 0000000000..9f0d1688ef --- /dev/null +++ b/BLOCKERS.md @@ -0,0 +1,221 @@ +# BLOCKERS.md - Phase 1 Implementation Status + +## Task 1: Module Scaffolding ✅ COMPLETE + +### Completed +- ✅ Module structure created (api/impl/test) +- ✅ Metro DI setup following Element X patterns +- ✅ WalletEntryPoint and WalletState APIs defined +- ✅ PaymentFlowNode placeholder with Appyx navigation +- ✅ FakeWalletEntryPoint for testing +- ✅ Cardano client library dependencies added +- ✅ ProGuard rules configured +- ✅ Basic unit tests added +- ✅ Pushed to Gitea phase1-dev branch + +--- + +## Task 2: Key Generation + Storage ✅ COMPLETE + +### Completed +- ✅ **CardanoNetworkConfig.kt** - Single object for testnet/mainnet config swap + - Currently configured for TESTNET (preprod) + - Change `NETWORK` to `CardanoNetwork.MAINNET` for production + - All derived values (Koios URL, explorer URL, address prefix) auto-switch + +- ✅ **CardanoKeyStorage** (interface + implementation) + - Per-session wallet isolation (key alias: `cardano_wallet_{sessionId}`) + - 24-word BIP-39 mnemonic generation using cardano-client-lib + - AES-GCM-256 encryption with Android Keystore-backed key + - `setUserAuthenticationRequired(true)` - biometric/PIN for every operation + - `setUserAuthenticationValidityDurationSeconds(-1)` - no grace period + - `setInvalidatedByBiometricEnrollment(true)` - invalidate on biometric change + - Methods: `generateWallet`, `importWallet`, `getMnemonic`, `getBaseAddress`, `getStakeAddress`, `deleteWallet` + +- ✅ **CardanoWalletManager** (interface + implementation) + - Key derivation using CIP-1852 via cardano-client-lib's Account class + - Path `m/1852'/1815'/0'/0/0` for external receiving address + - Path `m/1852'/1815'/0'/2/0` for staking key + - Shelley base address generation (payment + staking key hash) + - Uses CardanoNetworkConfig for network selection + - Exposes: `getAddress(sessionId)`, `getStakeAddress(sessionId)`, `getSpendingKey(sessionId)` + +- ✅ **SeedPhraseManager** (interface + implementation) + - 24-word mnemonic generation (256-bit entropy) + - Support for 12/15/18/21/24 word counts + - BIP-39 validation (checksum + wordlist) + - Word suggestions for autocomplete + - Normalization (whitespace, case) + - ⚠️ UI must apply `FLAG_SECURE` when displaying seed phrases (documented) + +- ✅ **FakeCardanoKeyStorage** for testing +- ✅ Unit tests for SeedPhraseManager, CardanoNetworkConfig, CardanoWalletManager + +### Decisions Made (per instructions) +- Wallet scope: **PER SESSION** (each Matrix account has its own wallet) +- Biometric change: **INVALIDATE** key + require wallet re-import/creation +- Network: **TESTNET** (preprod) - single config constant for easy mainnet swap + +### Not Verified (No Android SDK in build environment) +- ⚠️ Compilation with `./gradlew :features:wallet:impl:assemble` +- ⚠️ Unit tests with `./gradlew :features:wallet:impl:test` +- ⚠️ ktlint compliance +- ⚠️ Actual Android Keystore behavior (requires device/emulator) +- ⚠️ Biometric prompt integration (requires Activity context) + +### Security Notes +1. **Mnemonic never stored in plaintext** - Always encrypted with Keystore key +2. **Key material cleared after use** - `ByteArray.fill(0)` called where possible +3. **Per-session isolation** - Different Matrix accounts cannot access each other's wallets +4. **Biometric invalidation** - If user adds/removes fingerprints, wallet key becomes invalid +5. **No screenshots** - UI must apply FLAG_SECURE when showing seed phrase + +--- + +## Task 3: Koios Client ✅ COMPLETE + +### Completed +- ✅ **CardanoClient.kt** interface in `api/` module: + - `getBalance(address: String): Result` — balance in lovelace + - `getUtxos(address: String): Result>` — unspent outputs + - `submitTx(signedTxCbor: String): Result` — returns tx hash + - `getTxStatus(txHash: String): Result` — PENDING/CONFIRMED/FAILED + +- ✅ **Data models** in `api/`: + - `Utxo.kt` — txHash, outputIndex, amount, address + - `TxStatus.kt` — enum PENDING/CONFIRMED/FAILED + - `CardanoException.kt` — typed exceptions (NetworkException, RateLimitException, InvalidAddressException, TransactionNotFoundException, SubmissionFailedException, InsufficientFundsException, ApiException) + +- ✅ **KoiosCardanoClient.kt** implementation: + - Uses `BackendFactory.getKoiosBackendService()` from cardano-client-lib + - Testnet URL: `https://preprod.koios.rest/api/v1` (via CardanoNetworkConfig) + - Mainnet URL: `https://api.koios.rest/api/v1` (via CardanoNetworkConfig) + - 3 retries with exponential backoff (1s → 2s → 4s, max 10s) + - Basic rate limiting (100ms min between requests for Koios 100 req/10s limit) + - DI: `@ContributesBinding(SessionScope::class)` + - Error parsing: 429 → RateLimitException, 5xx → NetworkException, etc. + +- ✅ **FakeCardanoClient.kt** for testing: + - Configurable balances, UTxOs, transaction statuses + - Error simulation (network errors, rate limits, submit failures) + - Transaction lifecycle simulation (pending → confirmed → failed) + - Call counters for test verification + - Helper: `setupWallet(address, balance)` creates realistic UTxO set + +- ✅ **KoiosCardanoClientTest.kt** — 15+ unit tests: + - getBalance success, unknown address, network error, rate limit + - getUtxos success, empty result + - submitTx success, failure + - getTxStatus pending, confirmed, failed + - reset/state management + +- ✅ **CardanoWalletManager updated** to use CardanoClient: + - `refreshBalance()` now fetches real balance via Koios + - Updates WalletState with lovelace + formatted ADA string + +### Design Notes +- **No API key required** — Koios public API is free +- **Network config centralized** — Change `CardanoNetworkConfig.NETWORK` to swap testnet/mainnet +- **Hex CBOR for submitTx** — Accepts hex-encoded signed transaction bytes +- **UTxO pagination** — Limited to first 100 UTxOs (sufficient for typical wallets) + +### Potential Issues +- ⚠️ `getTxStatus` returns PENDING for unknown hashes (could be never-submitted or truly pending) +- ⚠️ Koios rate limit (100 req/10s) may need adjustment for heavy usage patterns +- ⚠️ No getProtocolParameters yet (needed for Task 4 fee calculation) + +--- + +## Task 4-6: See PHASE1-PLAN.md + +--- + +## Task 7: Timeline Payment Card ✅ COMPLETE + +### Completed +- ✅ **PaymentCardStatus.kt** — Enum for PENDING/CONFIRMED/FAILED states +- ✅ **TimelineItemPaymentContent.kt** — Data class implementing TimelineItemEventContent + - amountLovelace, addresses, txHash, status, network, isSentByMe + - Computed properties: amountAda, isTestnet, truncatedTxHash, explorerUrl + - Companion formatAda() helper +- ✅ **TimelineItemPaymentView.kt** — Compose UI for payment card + - Cardano icon (₳ symbol) + - Amount in ADA (formatted from lovelace) + - Status chip with spinner (pending), checkmark (confirmed), X (failed) + - Testnet badge when applicable + - Truncated tx hash (tappable → CardanoScan) + - View on explorer link for confirmed transactions + - @PreviewsDayNight with multiple preview states +- ✅ **TimelineItemPaymentContentTest.kt** — Unit tests for content model +- ✅ **Integration with TimelineItemEventContentView.kt** + +### Design Notes +- Payment cards use different colors for sent (primary) vs received (surface) +- Explorer URLs: preprod.cardanoscan.io for testnet, cardanoscan.io for mainnet +- Tx hash truncated to first 8 + last 8 chars for display + +--- + +## Task 8: Raw Event Handling ✅ COMPLETE (UPGRADED) + +### ✅ RESOLVED: SDK Raw Event API +**Previous blocker:** Matrix Rust SDK did not expose raw event sending or raw JSON access. + +**Resolution:** The SDK (version 26.03.24) now provides: +- `Timeline.sendRaw(eventType: String, content: String)` — Sends custom event types +- `MsgLikeKind.Other` with `eventType` field — Receives custom events +- `TimelineItemDebugInfo.originalJson` — Access to raw event JSON via debug info provider + +**Implementation updated to use proper raw events instead of text markers.** + +### Completed +- ✅ **PaymentEventSender.kt** — Interface for sending payment events +- ✅ **DefaultPaymentEventSender.kt** — Implementation using raw events + - Uses `timeline.sendRaw(eventType, content)` to send custom events + - Event type: `co.sulkta.payment.request` (reverse-domain format) + - Status updates: `co.sulkta.payment.status` + - No text marker hack — proper Matrix custom events +- ✅ **TimelineItemContentPaymentFactory.kt** — Parser for payment events + - `isPaymentEventType(eventType)` — Checks for payment event type + - `isStatusUpdateEventType(eventType)` — Checks for status update type + - `createFromRaw(json, isSentByMe)` — Parses raw JSON from custom events + - Supports both camelCase and snake_case field names + - Graceful error handling — returns null on malformed JSON +- ✅ **TimelineEventContentMapper.kt** — Maps `MsgLikeKind.Other` to `CustomEventContent` +- ✅ **TimelineItemContentFactory.kt** — Handles `CustomEventContent` for payments + - Gets raw JSON via `timelineItemDebugInfoProvider().originalJson` + - Delegates to paymentFactory for payment event types +- ✅ **CustomEventContent.kt** — New EventContent type for custom events +- ✅ **Timeline.sendRaw()** — Added to Timeline interface and RustTimeline implementation +- ✅ **FakePaymentEventSender.kt** — Test fake +- ✅ **TimelineItemContentPaymentFactoryTest.kt** — Updated unit tests + +### m.replace Status Updates +**Decision:** Status updates are sent as separate events of type `co.sulkta.payment.status`. + +**Future improvement:** When SDK exposes event relations, refactor to use m.replace for cleaner status update thread. + +### Benefits of Raw Event Approach +- ✅ Proper Matrix protocol compliance (custom event types, not hacked text) +- ✅ Non-wallet clients see "Unknown event" instead of JSON-in-text +- ✅ Clean separation of payment events from regular messages +- ✅ Events won't be indexed by message search +- ✅ No message length limits concern + +--- + +## Known Issues + +### Issue 1: Biometric Prompt Activity Context +The `CardanoKeyStorageImpl` uses `setUserAuthenticationRequired(true)` which will cause `UserNotAuthenticatedException` when accessing the key. The biometric prompt UI must be triggered from an Activity/Fragment context before calling `getMnemonic()`, `getSpendingKey()`, etc. + +**Solution:** Task 6 (Payment Flow UI) must call BiometricPrompt before invoking storage operations. + +### Issue 2: KeyPermanentlyInvalidatedException +If user changes biometric enrollment, the Keystore key is invalidated. Current behavior: throws exception, user must delete and recreate wallet. + +**Enhancement (future):** Show user-friendly message explaining why wallet became invalid and offer to re-import. + +--- + +*Last updated: 2026-03-27 - Task 2 complete* diff --git a/CHANGES.md b/CHANGES.md index 9775c2fea5..68848d491f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,213 +1,3 @@ -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 -============================= - - - -## 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 -============================= - - - -## What's Changed -### 🙌 Improvements -* Natural media viewer swiping order by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6431 -* Replace `rustls-platform-verifier-android.aar` with single class by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6610 -* Cleanup FetchPushForegroundService by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6577 -* cleaning: Remove join button from call notify timelineItemView by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/6603 -### 🐛 Bugfixes -* Fix crash when going back to threads list by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6620 -* audio: Let EC decide alone what communication device to use by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/6609 -* Fix media viewer bottom sheets not being scrollable by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6631 -### 🗣 Translations -* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6626 -### 📄 Documentation -* Updates to new features and some refactoring. by @mxandreas in https://github.com/element-hq/element-x-android/pull/6591 -### 🚧 In development 🚧 -* WIP : live location rendering by @ganfra in https://github.com/element-hq/element-x-android/pull/6611 -### Dependency upgrades -* Update dependency io.element.android:element-call-embedded to v0.19.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6593 -* Update dependency androidx.annotation:annotation-jvm to v1.10.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6596 -* Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6605 -* Update dependency com.google.firebase:firebase-bom to v34.12.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6604 -* Update actions/upload-artifact action to v7.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6614 -* Update plugin dependencycheck to v12.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6621 -* Update actions/github-script action to v9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6606 -* Update peter-evans/create-pull-request action to v8.1.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6615 -* Update dependencyAnalysis to v3.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6616 -* Update dependency org.matrix.rustcomponents:sdk-android to v26.04.21 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6635 -### Others -* Settings UI update. by @bmarty in https://github.com/element-hq/element-x-android/pull/6602 -* Support replying to messages with voice recordings by @kalix127 in https://github.com/element-hq/element-x-android/pull/6464 -* Add Black theme option for battery saving on OLED displays by @timurgilfanov in https://github.com/element-hq/element-x-android/pull/6441 -* Fix | When selecting earpiece twice in a row the proximity sensor get wrongly disabled by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/6627 -* Update wording of deactivate account screen by @bmarty in https://github.com/element-hq/element-x-android/pull/6633 - - -**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.04.3...v26.04.4 - Changes in Element X v26.04.3 ============================= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0126d89a9c..f0191d43e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,16 +2,16 @@ -* [Contributing to Element](#contributing-to-element) - * [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) - * [Submitting the PRs](#submitting-the-prs) - * [Android Studio settings](#android-studio-settings) - * [Compilation](#compilation) - * [Strings](#strings) +* [Contributing code to Matrix](#contributing-code-to-matrix) +* [Android Studio settings](#android-studio-settings) +* [Compilation](#compilation) +* [Strings](#strings) + * [I want to add new strings to the project](#i-want-to-add-new-strings-to-the-project) + * [I want to help translating Element](#i-want-to-help-translating-element) * [Element X Android Gallery](#element-x-android-gallery) +* [I want to add a new feature to Element X Android](#i-want-to-add-a-new-feature-to-element-x-android) +* [I want to submit a PR to fix an issue](#i-want-to-submit-a-pr-to-fix-an-issue) * [Kotlin](#kotlin) * [Changelog](#changelog) * [Code quality](#code-quality) @@ -29,67 +29,69 @@ -## Contributing to Element +## Developer onboarding + +For a detailed overview of the project, see [Developer Onboarding](./docs/_developer_onboarding.md). + +## Contributing code to Matrix + +If instead of contributing to the Element X Android project, you want to contribute to Synapse, the homeserver implementation, please read the [Synapse contribution guide](https://element-hq.github.io/synapse/latest/development/contributing_guide.html). Element X Android support can be found in this room: [![Element X Android Matrix room #element-x-android:matrix.org](https://img.shields.io/matrix/element-x-android:matrix.org.svg?label=%23element-x-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-x-android:matrix.org). The rest of the document contains specific rules for Matrix Android projects. -### I want to help translating Element - -To help translating, please go to [https://localazy.com/p/element](https://localazy.com/p/element). - -- If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please go to [https://localazy.com/p/element](https://localazy.com/p/element). -- If you want to fix an issue with an English string, please open an issue on the github project of Element X (Android or iOS). Only the core team can modify or add English strings. As an external contributor, if you want to add new strings, feel free to add an Android resource file to the project (for instance a file named `temporary.xml`), with a note in the description of the PR for the reviewer to integrate the String into `Localazy`. If accepted, the reviewer will add the String(s) for you, and then you can download them on your branch (following these [instructions](./tools/localazy/README.md#download-translations)) and remove the temporary file. Please follow the naming rules for the key. More details in [the dedicated section in this README.md](./tools/localazy/README.md#key-naming-rules) More information can be found [in this README.md](./tools/localazy/README.md). - -Once a language is sufficiently translated, it will be added to the app. The core team will decide when a language is sufficiently translated. - -### I want to fix a bug - -Please check if a corresponding issue exists, if not please create one. In both cases, let us know in the comment that you've started working on it. - -### I want to add a new feature or enhancement - -To make a great product with a great user experience, all the small efforts need to go in the same direction and be aligned and consistent with each other. - -Before making your contribution, please consider the following: - -* One product can’t do everything well. Element is focusing on private end-to-end encrypted messaging and voice - this can either be for consumers (e.g. friends and family) or for professional teams and organizations. Public forums and other types of chats without E2EE remain supported but are not the primary use case in case UX compromises need to be made. -* There are 3 platforms - Android, [iOS](https://github.com/element-hq/element-x-ios) and [Web/Desktop](https://github.com/element-hq/element-web). These platforms need to have feature parity and design consistency. For some features, supporting all platforms is a must have, in some cases exceptions can be made to have it on one platform only. -* To make sure your idea fits both from a design/solution and use case perspective, please open a new issue (or find an existing issue) in [element-meta](https://github.com/element-hq/element-meta/issues) repository describing the use case and how you plan to tackle it. Do not just describe what feature is missing, explain why the users need it with a couple of real life examples from the field. - * In case of an existing issue, please comment that you're planning to contribute. If you create a new issue, please specify that in the issue. In such a case we will try to review the issue ASAP and provide you with initial feedback so you can be confident if and at which conditions your contributions will be accepted. - -Once we know that you want to contribute and have confirmed that the new feature is overall aligned with the product direction, the designers of the core team will help you with the designs and any other type of guidance when it comes to the user experience. We will try to unblock you as quickly as we can, but it may not be instant. Having a clear understanding of the use case and the impact of the feature will help us with the prioritization and faster responses. - -Only once all of the above is met should you open a PR with your proposed changes. - -## Developer onboarding - -For a detailed overview of the project, see [Developer Onboarding](./docs/_developer_onboarding.md). - -### Submitting the PRs - -Please have a look in the [dedicated documentation](./docs/pull_request.md) about pull request. - -### Android Studio settings +## Android Studio settings Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`). Please ensure that you're using the project formatting rules (which are in the project at .idea/codeStyles/), and format the file before committing them. -### Compilation +## Compilation This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`. -### Strings +## Strings The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with Element X iOS. +### I want to add new strings to the project + +Only the core team can modify or add English strings to Localazy. As an external contributor, if you want to add new strings, feel free to add an Android resource file to the project (for instance a file named `temporary.xml`), with a note in the description of the PR for the reviewer to integrate the String into `Localazy`. If accepted, the reviewer will add the String(s) for you, and then you can download them on your branch (following these [instructions](./tools/localazy/README.md#download-translations)) and remove the temporary file. + +Please follow the naming rules for the key. More details in [the dedicated section in this README.md](./tools/localazy/README.md#key-naming-rules) + +### I want to help translating Element + +To help translating, please go to [https://localazy.com/p/element](https://localazy.com/p/element). + +- If you want to fix an issue with an English string, please open an issue on the github project of Element X (Android or iOS). Only the core team can modify or add English strings. +- If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please go to [https://localazy.com/p/element](https://localazy.com/p/element). + +More information can be found [in this README.md](./tools/localazy/README.md). + +Once a language is sufficiently translated, it will be added to the app. The core team will decide when a language is sufficiently translated. + ### Element X Android Gallery Once added to Localazy, translations can be checked screen per screen using our tool Element X Android Gallery, available at https://element-hq.github.io/element-x-android/. Localazy syncs occur every Monday and the screenshots on this page are generated every Tuesday, so you'll have to wait to see your change appearing on Element X Android Gallery. +## I want to add a new feature to Element X Android + +Thank you for contributing to the project! Please have a look in the [dedicated documentation](./docs/pull_request.md) about pull request. + +Also, please keep in mind that any feature added to Element X Android needs to be added to [the iOS client](https://github.com/element-hq/element-x-ios) too, unless it's related to an Android OS only behaviour. + +**IMPORTANT:** if you are adding new screens or modifying existing ones, this needs acceptance from the product and design teams before being merged. For this, it's better to start with a [feature request issue](https://github.com/element-hq/element-x-android/issues/new?template=enhancement.yml) describing the change you want to make and the motivation behind it instead of directly creating a pull request. This will allow the product and design teams to give feedback on the change before you start working on it, and avoid you doing work that might end up being rejected. + +## I want to submit a PR to fix an issue + +Please have a look in the [dedicated documentation](./docs/pull_request.md) about pull request. + +Please check if a corresponding issue exists. If yes, please let us know in a comment that you're working on it. +If an issue does not exist yet, it may be relevant to open a new issue and let us know that you're implementing it. + ### Kotlin This project is full Kotlin. Please do not write Java classes. diff --git a/PHASE1-STATUS.md b/PHASE1-STATUS.md new file mode 100644 index 0000000000..670057ee14 --- /dev/null +++ b/PHASE1-STATUS.md @@ -0,0 +1,34 @@ +# Phase 1 Status — COMPLETE ✅ + +## Verification Date +2026-03-28 + +## What Was Verified +- APK: `app-gplay-x86_64-debug.apk` built from `phase1-dev` branch +- Installed on Android emulator `budtmo/docker-android:emulator_14.0` (emulator-5554) +- Signed in as `@testbot-elementx:sulkta.com` via OIDC (MAS at mas.sulkta.com) +- Opened DM room with `@cobb:sulkta.com` +- Typed `/pay` in message composer + +## Result +✅ Slash command autocomplete appeared showing: + - Command: `/pay` + - Description: "Send ADA to someone" + +## Phase 1 Bar (Option A) — All Conditions Met +- [x] App launches without crash +- [x] `/pay` appears in slash command autocomplete +- [x] Payment screens navigable (wired in DI graph) +- [x] No live testnet transaction required + +## Build Info +- Gradle task: `:app:assembleGplayDebug` +- Branch: `phase1-dev` +- Final commit: `ad89eddfea` +- Build image: `mingc/android-build-box:latest` (Java 21) + +## Key Fixes Applied +1. Metro DI scope mismatch: CardanoWalletManager removed CardanoClient dep (AppScope vs SessionScope) +2. WalletState constructor: all required fields populated +3. Packaging conflict: moshi-kotlin-codegen/lombok META-INF pickFirst +4. Build flavor: assembleGplayDebug (not fdroid, not plain assembleDebug) diff --git a/SYNC.md b/SYNC.md new file mode 100644 index 0000000000..35996377c9 --- /dev/null +++ b/SYNC.md @@ -0,0 +1,78 @@ +# Repo topology + upstream sync procedure + +This repo is a fork of [`element-hq/element-x-android`](https://github.com/element-hq/element-x-android) +with a native Cardano wallet module added. The history is structured so that +staying current with upstream — and one day proposing our additions back — +stays possible. + +## Branches + +| Branch | Role | +|--------|------| +| `main` | Tracks the upstream commit we are currently based on. Fast-forwarded to `upstream/develop` when we deliberately pull in changes. Nothing coop-specific lives here. | +| `wallet` | `main` + all our wallet work. This is what we build APKs from. Linear history on top of `main`; rebased whenever `main` moves. | +| `archive/project-docs` | Frozen snapshot of the planning docs and screenshots that lived on the original orphan `main` branch. Not part of the active graph. | + +When we ever want a clean "everything we'd propose upstream" branch, we cherry-pick +the wallet commits off `wallet` onto a fresh branch rooted at `main`. Because every +current commit on `wallet` is wallet-module work, that split is simple. + +## Remotes + +`origin` → this Gitea repo (LAN, via the Rackham SSH tunnel when working remotely). + +Add upstream on any local clone: + +```bash +git remote add upstream https://github.com/element-hq/element-x-android.git +git fetch upstream +``` + +## Sync with upstream + +When you want to pick up the latest from `element-hq/element-x-android`: + +```bash +# 1. Get the latest from upstream +git fetch upstream + +# 2. Fast-forward main to upstream/develop +git checkout main +git merge --ff-only upstream/develop +git push origin main + +# 3. Rebase wallet onto the new main +git checkout wallet +git rebase main +# → resolve conflicts, one commit at a time +# → conflict surface is small but real: our integration touches +# libraries/matrix/{api,impl}, libraries/textcomposer/impl, +# libraries/eventformatter/impl, libraries/mediaviewer/impl + +# 4. Build + test the APK before force-pushing +./gradlew :app:assembleFdroidDebug # or mainnet variant + +# 5. Push the rebased wallet branch (force-with-lease, not plain force) +git push --force-with-lease origin wallet +``` + +If the rebase gets ugly, abort and try merging instead: + +```bash +git rebase --abort +git merge upstream/develop +# resolves in one shot, one merge commit, less clean history +``` + +## Why not a Gitea mirror? + +Gitea only lets you configure a pull-mirror at repo-creation time, and mirroring +a whole repo also means we can't commit to it. We want to keep our own commits, +so upstream stays as a git remote you fetch from manually. + +## License + +Upstream is **AGPL-3.0**. Every binary we hand out must be accompanied by the +corresponding source under the same license. Keeping this Gitea repo accessible +to recipients of the APK satisfies that. Don't ship binaries without also making +the source reachable. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index da90ec82e4..bf82b7d01f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,13 +103,13 @@ android { logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName) [$buildType]") buildTypes { - val oAuthRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android" + val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android" getByName("debug") { resValue("string", "app_name", "$baseAppName dbg") resValue( "string", "login_redirect_scheme", - "$oAuthRedirectSchemeBase.debug", + "$oidcRedirectSchemeBase.debug", ) applicationIdSuffix = ".debug" signingConfig = signingConfigs.getByName("debug") @@ -120,7 +120,7 @@ android { resValue( "string", "login_redirect_scheme", - oAuthRedirectSchemeBase, + oidcRedirectSchemeBase, ) signingConfig = signingConfigs.getByName("debug") @@ -157,7 +157,7 @@ android { resValue( "string", "login_redirect_scheme", - "$oAuthRedirectSchemeBase.nightly", + "$oidcRedirectSchemeBase.nightly", ) matchingFallbacks += listOf("release") signingConfig = signingConfigs.getByName("nightly") @@ -208,6 +208,7 @@ android { packaging { resources.pickFirsts += setOf( "META-INF/versions/9/OSGI-INF/MANIFEST.MF", + "META-INF/gradle/incremental.annotation.processors", ) jniLibs { @@ -315,6 +316,11 @@ licensee { allowUrl("https://asm.ow2.io/license.html") allowUrl("https://www.gnu.org/licenses/agpl-3.0.txt") allowUrl("https://github.com/mhssn95/compose-color-picker/blob/main/LICENSE") + allowUrl("https://opensource.org/licenses/mit-license.php") + allowUrl("https://github.com/javaee/javax.annotation/blob/master/LICENSE") + allowUrl("https://www.bouncycastle.org/licence.html") + allowUrl("https://projectlombok.org/LICENSE") + allow("CC0-1.0") ignoreDependencies("com.github.matrix-org", "matrix-analytics-events") // Ignore dependency that are not third-party licenses to us. ignoreDependencies(groupId = "io.element.android") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d63e18ec1a..6041fbb118 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/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index a882dd8769..502518bf3c 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -71,7 +71,6 @@ class MainActivity : NodeActivity() { }.collectAsState(SemanticColorsLightDark.default) ElementThemeApp( appPreferencesStore = appBindings.preferencesStore(), - featureFlagService = appBindings.featureFlagService(), compoundLight = colors.light, compoundDark = colors.dark, buildMeta = appBindings.buildMeta() diff --git a/app/src/main/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProvider.kt b/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt similarity index 82% rename from app/src/main/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProvider.kt rename to app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt index 16db564aaf..ad4f9a47b2 100644 --- a/app/src/main/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProvider.kt +++ b/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.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.OAuthRedirectUrlProvider +import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.x.R @ContributesBinding(AppScope::class) -class DefaultOAuthRedirectUrlProvider( +class DefaultOidcRedirectUrlProvider( private val stringProvider: StringProvider, -) : OAuthRedirectUrlProvider { +) : OidcRedirectUrlProvider { override fun provide() = buildString { append(stringProvider.getString(R.string.login_redirect_scheme)) append(":/") diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index a77f42817d..c95b3a5cc0 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -2,7 +2,6 @@ - diff --git a/app/src/test/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProviderTest.kt b/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt similarity index 89% rename from app/src/test/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProviderTest.kt rename to app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt index c26e3dc692..18567355d2 100644 --- a/app/src/test/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProviderTest.kt +++ b/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.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 DefaultOAuthRedirectUrlProviderTest { +class DefaultOidcRedirectUrlProviderTest { @Test fun `test provide`() { val stringProvider = FakeStringProvider( defaultResult = "str" ) - val sut = DefaultOAuthRedirectUrlProvider( + val sut = DefaultOidcRedirectUrlProvider( stringProvider = stringProvider, ) val result = sut.provide() diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts index 64b9b76a14..45496acb77 100644 --- a/appconfig/build.gradle.kts +++ b/appconfig/build.gradle.kts @@ -48,8 +48,6 @@ android { } dependencies { - implementation(libs.coroutines.core) implementation(libs.androidx.annotationjvm) - implementation(libs.androidx.corektx) implementation(projects.libraries.matrix.api) } diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ProtectionConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ProtectionConfig.kt deleted file mode 100644 index f6ad71eeb1..0000000000 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/ProtectionConfig.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.appconfig - -object ProtectionConfig { - /** - * The maximum length of a room name, to limit attack vectors in room invite. - */ - const val MAX_ROOM_NAME_LENGTH = 128 -} diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 7440ecd2bf..24a0355b3f 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -33,14 +33,13 @@ dependencies { implementation(projects.libraries.deeplink.api) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.api) - implementation(projects.libraries.oauth.api) + implementation(projects.libraries.oidc.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) @@ -60,7 +59,7 @@ dependencies { testImplementation(projects.features.login.test) testImplementation(projects.features.share.test) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.oauth.test) + testImplementation(projects.libraries.oidc.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 14230d7c5a..44c1060e10 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -54,7 +54,6 @@ 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 @@ -78,7 +77,6 @@ import io.element.android.libraries.designsystem.theme.ElementThemeApp import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope -import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -146,13 +144,11 @@ class LoggedInFlowNode( private val syncService: SyncService, private val enterpriseService: EnterpriseService, private val appPreferencesStore: AppPreferencesStore, - private val featureFlagService: FeatureFlagService, private val buildMeta: BuildMeta, snackbarDispatcher: SnackbarDispatcher, private val analyticsService: AnalyticsService, private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher, private val createRoomEntryPoint: CreateRoomEntryPoint, - private val activeLiveLocationShareManager: ActiveLiveLocationShareManager, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Placeholder, @@ -213,7 +209,6 @@ class LoggedInFlowNode( super.onBuilt() lifecycleScope.launch { sessionEnterpriseService.init() - activeLiveLocationShareManager.setup() } lifecycle.subscribe( onCreate = { @@ -222,6 +217,7 @@ 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 } @@ -382,13 +378,9 @@ class LoggedInFlowNode( } is NavTarget.Room -> { val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback { - override fun onDone() { - backstack.pop() - } - - override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) { + override fun navigateToRoom(roomId: RoomId, serverNames: List) { lifecycleScope.launch { - attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = clearBackStack) + attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = false) } } @@ -679,7 +671,6 @@ class LoggedInFlowNode( }.collectAsState(SemanticColorsLightDark.default) ElementThemeApp( appPreferencesStore = appPreferencesStore, - featureFlagService = featureFlagService, compoundLight = colors.light, compoundDark = colors.dark, buildMeta = buildMeta, 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 acf7b66db9..0e458d3b9c 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.oauth.api.OAuthAction -import io.element.android.libraries.oauth.api.OAuthActionFlow +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcActionFlow 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 oAuthActionFlow: OAuthActionFlow, + private val oidcActionFlow: OidcActionFlow, 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.OAuth -> onOAuthAction(resolvedIntent.oAuthAction) + is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction) is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData) is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.shareIntentData) } @@ -529,8 +529,8 @@ class RootFlowNode( } } - private fun onOAuthAction(oAuthAction: OAuthAction) { - oAuthActionFlow.post(oAuthAction) + private fun onOidcAction(oidcAction: OidcAction) { + oidcActionFlow.post(oidcAction) } 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 1ce423a569..9b1bbd1b81 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,14 +89,16 @@ class SyncOrchestrator( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun observeStates() = coroutineScope.launch { Timber.tag(tag).d("start observing the app and network state") - val isAppActiveFlows = listOf( + + val isAppActiveFlow = combine( appForegroundStateService.isInForeground, appForegroundStateService.isInCall, appForegroundStateService.isSyncingNotificationEvent, appForegroundStateService.hasRingingCall, - appForegroundStateService.isSharingLiveLocation - ) - val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } } + ) { isInForeground, isInCall, isSyncingNotificationEvent, hasRingingCall -> + isInForeground || isInCall || isSyncingNotificationEvent || hasRingingCall + } + 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 ee316f00aa..6844db3ed6 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.oauth.api.OAuthAction -import io.element.android.libraries.oauth.api.OAuthIntentResolver +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcIntentResolver import timber.log.Timber sealed interface ResolvedIntent { data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent - data class OAuth(val oAuthAction: OAuthAction) : ResolvedIntent + data class Oidc(val oidcAction: OidcAction) : 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 oAuthIntentResolver: OAuthIntentResolver, + private val oidcIntentResolver: OidcIntentResolver, 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 OAuth? - val oAuthAction = oAuthIntentResolver.resolve(intent) - if (oAuthAction != null) return ResolvedIntent.OAuth(oAuthAction) + // Coming during login using Oidc? + val oidcAction = oidcIntentResolver.resolve(intent) + if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction) val actionViewData = intent .takeIf { it.action == Intent.ACTION_VIEW } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 752d10e7a9..8dc2de5e4e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.MatrixClient 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.roomlist.RoomListService import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncService @@ -176,6 +177,7 @@ class LoggedInPresenter( } private fun CoroutineScope.preloadAccountManagementUrl() = launch { - matrixClient.getAccountManagementUrl(null) + matrixClient.getAccountManagementUrl(AccountManagementAction.Profile) + matrixClient.getAccountManagementUrl(AccountManagementAction.DevicesList) } } 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 dbe53b75ac..febd15e9c2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -82,8 +82,7 @@ class JoinedRoomLoadedFlowNode( plugins = plugins, ), DependencyInjectionGraphOwner { interface Callback : Plugin { - fun onDone() - fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean = false) + fun navigateToRoom(roomId: RoomId, serverNames: List) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun navigateToGlobalNotificationSettings() fun navigateToDeveloperSettings() @@ -143,10 +142,6 @@ 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() } @@ -155,7 +150,7 @@ class JoinedRoomLoadedFlowNode( callback.navigateToDeveloperSettings() } - override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) { + override fun navigateToRoom(roomId: RoomId, serverNames: List) { callback.navigateToRoom(roomId, serverNames) } diff --git a/appnav/src/main/res/values-ca/translations.xml b/appnav/src/main/res/values-ca/translations.xml deleted file mode 100644 index d251b3a6b1..0000000000 --- a/appnav/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - "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/main/res/values-zh/translations.xml b/appnav/src/main/res/values-zh/translations.xml index f6eac30310..406471196e 100644 --- a/appnav/src/main/res/values-zh/translations.xml +++ b/appnav/src/main/res/values-zh/translations.xml @@ -1,6 +1,6 @@ - "注销并升级" + "登出并升级" "%1$s 不再支持旧协议。请注销并重新登录以继续使用该应用程序。" - "你的主服务器不再支持旧协议。请注销并重新登录以继续使用此 app。" + "您的服务器不再支持旧协议。请登出并重新登录以继续使用此应用。" 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 451ca279f8..576e1aaea6 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,8 +26,8 @@ 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.oauth.api.OAuthAction -import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.test.FakeOidcIntentResolver import io.element.android.tests.testutils.lambda.lambdaError import org.junit.Test import org.junit.runner.RunWith @@ -170,9 +170,9 @@ class IntentResolverTest { } @Test - fun `test resolve OAuth`() { + fun `test resolve oidc`() { val sut = createIntentResolver( - oAuthIntentResolverResult = { OAuthAction.GoBack() }, + oidcIntentResolverResult = { OidcAction.GoBack() }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -180,8 +180,8 @@ class IntentResolverTest { } val result = sut.resolve(intent) assertThat(result).isEqualTo( - ResolvedIntent.OAuth( - oAuthAction = OAuthAction.GoBack() + ResolvedIntent.Oidc( + oidcAction = OidcAction.GoBack() ) ) } @@ -194,7 +194,7 @@ class IntentResolverTest { val sut = createIntentResolver( loginIntentResolverResult = { null }, permalinkParserResult = { permalinkData }, - oAuthIntentResolverResult = { null }, + oidcIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -213,7 +213,7 @@ class IntentResolverTest { val sut = createIntentResolver( permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }, loginIntentResolverResult = { null }, - oAuthIntentResolverResult = { null }, + oidcIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -230,7 +230,7 @@ class IntentResolverTest { ) val sut = createIntentResolver( permalinkParserResult = { permalinkData }, - oAuthIntentResolverResult = { null }, + oidcIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_BATTERY_LOW @@ -244,7 +244,7 @@ class IntentResolverTest { fun `test incoming share simple`() { val shareIntentData = ShareIntentData.PlainText("Hello") val sut = createIntentResolver( - oAuthIntentResolverResult = { null }, + oidcIntentResolverResult = { null }, onIncomingShareIntent = { shareIntentData }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { @@ -260,7 +260,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( - oAuthIntentResolverResult = { null }, + oidcIntentResolverResult = { null }, onIncomingShareIntent = { shareIntentData }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { @@ -277,7 +277,7 @@ class IntentResolverTest { val sut = createIntentResolver( permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }, loginIntentResolverResult = { null }, - oAuthIntentResolverResult = { null }, + oidcIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -292,7 +292,7 @@ class IntentResolverTest { val aLoginParams = LoginParams("accountProvider", null) val sut = createIntentResolver( loginIntentResolverResult = { aLoginParams }, - oAuthIntentResolverResult = { null }, + oidcIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -306,7 +306,7 @@ class IntentResolverTest { deeplinkParserResult: DeeplinkData? = null, permalinkParserResult: (String) -> PermalinkData = { lambdaError() }, loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() }, - oAuthIntentResolverResult: (Intent) -> OAuthAction? = { lambdaError() }, + oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() }, onIncomingShareIntent: (Intent) -> ShareIntentData? = { null }, ): IntentResolver { return IntentResolver( @@ -314,8 +314,8 @@ class IntentResolverTest { loginIntentResolver = FakeLoginIntentResolver( parseResult = loginIntentResolverResult, ), - oAuthIntentResolver = FakeOAuthIntentResolver( - resolveResult = oAuthIntentResolverResult, + oidcIntentResolver = FakeOidcIntentResolver( + resolveResult = oidcIntentResolverResult, ), 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 18c8cfd7b9..d147a4ed68 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -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.oauth.AccountManagementAction +import io.element.android.libraries.matrix.api.oidc.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 @@ -71,7 +71,7 @@ class LoggedInPresenterTest { } @Test - fun `present - ensure that account url is preloaded`() = runTest { + fun `present - ensure that account urls are preloaded`() = runTest { val accountManagementUrlResult = lambdaRecorder> { Result.success("aUrl") } val matrixClient = FakeMatrixClient( accountManagementUrlResult = accountManagementUrlResult, @@ -81,8 +81,11 @@ class LoggedInPresenterTest { ).test { awaitItem() advanceUntilIdle() - accountManagementUrlResult.assertions().isCalledOnce() - .with(value(null)) + accountManagementUrlResult.assertions().isCalledExactly(2) + .withSequence( + listOf(value(AccountManagementAction.Profile)), + listOf(value(AccountManagementAction.DevicesList)), + ) } } 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 ec36f3d32d..2f17071870 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt @@ -13,8 +13,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.tests.testutils.lambda.lambdaError class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback { - override fun onDone() = lambdaError() - override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) = lambdaError() + override fun navigateToRoom(roomId: RoomId, serverNames: List) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun navigateToGlobalNotificationSettings() = lambdaError() override fun navigateToDeveloperSettings() = lambdaError() diff --git a/build.gradle.kts b/build.gradle.kts index 474b868eda..f699378d54 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,15 +46,12 @@ allprojects { config.from(files("$rootDir/tools/detekt/detekt.yml")) } dependencies { - detektPlugins("io.nlopez.compose.rules:detekt:0.5.8") + detektPlugins("io.nlopez.compose.rules:detekt:0.5.6") detektPlugins(project(":tests:detekt-rules")) } tasks.withType().configureEach { exclude("io/element/android/tests/konsist/failures/**") - - // This file comes from another project and we want to keep it as close to the original as possible - exclude("org/rustls/platformverifier/**") } // KtLint @@ -82,9 +79,6 @@ allprojects { // This file comes from another project and we want to keep it as close to the original as possible exclude("**/SafeChildrenTransitionScope.kt") - - // This file comes from another project and we want to keep it as close to the original as possible - exclude("org/rustls/platformverifier/**") } } // Dependency check diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index 74020ead98..a264bfec63 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -144,11 +144,6 @@ Prerequisites: export ANDROID_HOME=$HOME/android/sdk ``` -* On macos ensure gnu-getopt is installed - ``` - brew install gnu-getopt - ``` - You can then build the Rust SDK by running the script [`tools/sdk/build-rust-sdk`](../tools/sdk/build-rust-sdk). Type `./tools/sdk/build-rust-sdk --help` for help. diff --git a/docs/oauth.md b/docs/oidc.md similarity index 81% rename from docs/oauth.md rename to docs/oidc.md index 1080c64b0e..23709b608c 100644 --- a/docs/oauth.md +++ b/docs/oidc.md @@ -1,4 +1,4 @@ -This file contains some rough notes about OAuth implementation, with some examples of actual data. +This file contains some rough notes about Oidc 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 OAuthData (from presentUrl callback): +Example of OidcData (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 -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 +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 Test server: diff --git a/enterprise b/enterprise index 6781da90aa..cdde60c158 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 6781da90aae61cf77dcdbc543e18d76411d578b4 +Subproject commit cdde60c158ecd0987a3ba6fd79a4617551aff463 diff --git a/fastlane/metadata/android/en-US/changelogs/202604040.txt b/fastlane/metadata/android/en-US/changelogs/202604040.txt deleted file mode 100644 index cbb77b7606..0000000000 --- a/fastlane/metadata/android/en-US/changelogs/202604040.txt +++ /dev/null @@ -1,2 +0,0 @@ -Main changes in this version: several bug fixes. -Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202605000.txt b/fastlane/metadata/android/en-US/changelogs/202605000.txt deleted file mode 100644 index a4b397f1bb..0000000000 --- a/fastlane/metadata/android/en-US/changelogs/202605000.txt +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 0ad08f5b4d..0000000000 --- a/fastlane/metadata/android/en-US/changelogs/202605010.txt +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index a4b397f1bb..0000000000 --- a/fastlane/metadata/android/en-US/changelogs/202605020.txt +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 9f352d28ce..0000000000 --- a/features/analytics/api/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - "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 e1495271d3..5554ee162f 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 8f1c1699d9..e5f9fccd66 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 deleted file mode 100644 index 5a2b633075..0000000000 --- a/features/analytics/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - "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 162e01ecb0..2cee69962c 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 678d506287..d18650654b 100644 --- a/features/analytics/impl/src/main/res/values-zh/translations.xml +++ b/features/analytics/impl/src/main/res/values-zh/translations.xml @@ -2,9 +2,9 @@ "我们不会记录或分析任何个人数据" "共享匿名使用数据以帮助我们排查问题。" - "你可以点击 %1$s 阅读我们的所有条款。" + "您可以阅读我们的所有条款 %1$s。" "此处" "可以随时关闭此功能" - "我们不会与第三方共享你的数据" + "我们不会与第三方共享您的数据" "帮助改进 %1$s" diff --git a/features/announcement/impl/src/main/res/values-bg/translations.xml b/features/announcement/impl/src/main/res/values-bg/translations.xml new file mode 100644 index 0000000000..853cf5f027 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-bg/translations.xml @@ -0,0 +1,4 @@ + + + "Присъединете се към обществени пространства" + diff --git a/features/announcement/impl/src/main/res/values-cs/translations.xml b/features/announcement/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..cf7ead1962 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,11 @@ + + + "Zobrazit prostory, které jste vytvořili nebo ke kterým jste se připojili" + "Přijmout nebo odmítnout pozvánky do prostorů" + "Objevte všechny místnosti, do kterých můžete vstoupit ve svých prostorech" + "Připojit se k veřejným prostorům" + "Opustit všechny prostory, ke kterým jste se připojili" + "Filtrování, vytváření a správa prostorů bude brzy k dispozici." + "Vítejte v beta verzi prostorů! S touto první verzí můžete:" + "Představujeme prostory" + diff --git a/features/announcement/impl/src/main/res/values-da/translations.xml b/features/announcement/impl/src/main/res/values-da/translations.xml new file mode 100644 index 0000000000..76540962e1 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,11 @@ + + + "Se klynger, du har oprettet eller tilmeldt dig" + "Acceptere eller afvise invitationer til klynger" + "Finde alle rum, du kan deltage i, i dine klynger" + "Deltage i offentlige klynger" + "Forlade de klynger, du har tilsluttet dig" + "Filtrering, oprettelse og administration af klynger kommer snart." + "Velkommen til betaversionen af Klynger! Med denne første version kan du:" + "Introduktion til Klynger" + diff --git a/features/announcement/impl/src/main/res/values-de/translations.xml b/features/announcement/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..11f5f3a99c --- /dev/null +++ b/features/announcement/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,11 @@ + + + "Von dir erstellte oder beigetretene Spaces anzeigen" + "Einladungen zu Spaces annehmen oder ablehnen" + "Chats innerhalb deiner Spaces entdecken, um ihnen beizutreten" + "Öffentlichen Spaces beitreten" + "Spaces verlassen, bei denen du Mitglied bist" + "Das Filtern, Erstellen und Verwalten von Spaces ist bald verfügbar." + "Willkommen bei der Beta-Version von Spaces! Mit dieser ersten Version kannst du:" + "Einführung in Spaces" + diff --git a/features/announcement/impl/src/main/res/values-el/translations.xml b/features/announcement/impl/src/main/res/values-el/translations.xml new file mode 100644 index 0000000000..bdeb821efb --- /dev/null +++ b/features/announcement/impl/src/main/res/values-el/translations.xml @@ -0,0 +1,11 @@ + + + "Δείτε τους χώρους που έχετε δημιουργήσει ή στους οποίους έχετε εγγραφεί" + "Να αποδεχθείτε ή να απορρίψετε προσκλήσεις σε χώρους" + "Να ανακαλύψτε όλες τις αίθουσες που μπορείτε να συμμετάσχετε στους χώρους σας" + "Να συμμετάσχετε σε δημόσιους χώρους" + "Να αποχωρήστε από χώρους στους οποίους έχετε συμμετάσχει" + "Το φιλτράρισμα, η δημιουργία και η διαχείριση χώρων θα είναι σύντομα διαθέσιμα." + "Καλώς ορίσατε στην δοκιμαστική έκδοση των Χώρων! Με αυτήν την πρώτη έκδοση μπορείτε:" + "Παρουσιάζοντας τους Χώρους" + diff --git a/features/announcement/impl/src/main/res/values-et/translations.xml b/features/announcement/impl/src/main/res/values-et/translations.xml new file mode 100644 index 0000000000..ee2ba9c3b4 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-et/translations.xml @@ -0,0 +1,11 @@ + + + "Vaadata kogukondi, mille oled loonud või millega oled liitunud" + "Nõustuda kutsetega liitumiseks kogukonnaga või sellest keelduda" + "Uurida neis kogukondades leiduvaid jututube ning nendega liituda" + "Liituda avalike kogukondadega" + "Lahkuda kogukonnast, millega oled liitunud" + "Kogukondade filtreerimine, loomine ja haldamine lisandub peagi" + "Tere tulemast kasutama kogukondade beetaversiooni! Selles esimeses versioonis saad sa:" + "Võtame kasutusele kogukonnad" + diff --git a/features/announcement/impl/src/main/res/values-fa/translations.xml b/features/announcement/impl/src/main/res/values-fa/translations.xml new file mode 100644 index 0000000000..2e8902ae23 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-fa/translations.xml @@ -0,0 +1,11 @@ + + + "دیدن فضاهایی که ساخته یا پیوسته‌اید" + "پذیرش یا رد دعوت‌ها به فضاها" + "کشف تمامی اتاق‌هایی که می‌توانید در فضاهایتان بپیوندید" + "پیوستن به فضاهای عمومی" + "ترک هر فضایی که پیوسته‌اید" + "پالایش، ایجاد و مدیریت کردن فضاها به زودی." + "به نگارش آزمایشی فضاها خوش آمدید! در این نگارش می‌توانید:" + "معرّفی فضاها" + diff --git a/features/announcement/impl/src/main/res/values-fi/translations.xml b/features/announcement/impl/src/main/res/values-fi/translations.xml new file mode 100644 index 0000000000..8e7674487f --- /dev/null +++ b/features/announcement/impl/src/main/res/values-fi/translations.xml @@ -0,0 +1,11 @@ + + + "Nähdä luomasi tai liittymäsi tilat" + "Hyväksyä tai hylätä kutsuja tiloihin" + "Löytää kaikki huoneet, joihin voit liittyä tiloissasi" + "Liittyä julkisiin tiloihin" + "Poistua mistä tahansa tilasta, johon olet liittynyt" + "Tilojen suodatus, luominen ja hallinta on tulossa pian." + "Tervetuloa tilojen beetaversioon! Tämän ensimmäisen version avulla voit:" + "Esittelyssä tilat" + diff --git a/features/announcement/impl/src/main/res/values-fr/translations.xml b/features/announcement/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..7e042c65ff --- /dev/null +++ b/features/announcement/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,11 @@ + + + "Voir les espaces que vous avez créés ou rejoints" + "Accepter ou refuser les invitations aux espaces" + "Découvrir les salons que vous pouvez joindre depuis vos espaces" + "Rejoindre les espaces publics" + "Quitter les espaces dont vous êtes membre." + "Le filtrage, la création et la gestion des espaces seront bientôt disponibles." + "Bienvenue dans la version bêta des espaces! Avec cette première version, vous pourrez :" + "Ajout des espaces" + diff --git a/features/announcement/impl/src/main/res/values-hr/translations.xml b/features/announcement/impl/src/main/res/values-hr/translations.xml new file mode 100644 index 0000000000..e78f29f19a --- /dev/null +++ b/features/announcement/impl/src/main/res/values-hr/translations.xml @@ -0,0 +1,11 @@ + + + "Pregledajte prostore koje ste stvorili ili kojima ste se pridružili" + "Prihvatite ili odbijte pozivnice za prostore" + "Otkrijte sve sobe kojima se možete pridružiti u svojim prostorima" + "Pridružite se javnim prostorima" + "Napustite sve prostore kojima ste se pridružili" + "Uskoro stiže filtriranje i stvaranje prostora te upravljanje njima." + "Dobrodošli u beta inačicu prostora! S ovom prvom inačicom možete:" + "Predstavljamo prostore" + diff --git a/features/announcement/impl/src/main/res/values-hu/translations.xml b/features/announcement/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..b09f70419b --- /dev/null +++ b/features/announcement/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,11 @@ + + + "Az Ön által létrehozott vagy csatlakozott térek megtekintése" + "A meghívások elfogadására vagy elutasítására a terekhez" + "Szobák felfedezése a terekben, amelyekhez csatlakozhat" + "Csatlakozás nyilvános terekhez" + "Terek elhagyása" + "Terek szűrése, készítése és kezelése hamarosan érkezik." + "Üdvözöljük a tér béta verziójában! Ezzel az első verzióval a következőket teheti:" + "Bemutatkoznak a terek" + diff --git a/features/announcement/impl/src/main/res/values-it/translations.xml b/features/announcement/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..584ddcdf21 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,11 @@ + + + "Visualizza gli spazi che hai creato o a cui partecipi" + "Accetta o rifiuta gli inviti agli spazi" + "Scopri tutte le stanze a cui puoi partecipare nei tuoi spazi" + "Unisciti agli spazi pubblici" + "Lascia tutti gli spazi a cui ti sei unito" + "A breve saranno disponibili le funzionalità di filtraggio, creazione e gestione degli spazi." + "Benvenuti alla versione beta degli Spazi! Con questa prima versione potrete:" + "Ti presentiamo gli Spazi" + diff --git a/features/announcement/impl/src/main/res/values-ja/translations.xml b/features/announcement/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..47273cc12d --- /dev/null +++ b/features/announcement/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,11 @@ + + + "作成または参加したスペースを表示できます" + "スペースへの招待を受諾または拒否できます" + "スペース内の参加可能なルームを検索できます" + "公開スペースに参加できます" + "参加したスペースを退出できます" + "スペースの作成や管理, フィルター検索は近日実装予定です。" + "ベータ版のスペースにようこそ。この最新のバージョンでは:" + "スペースの紹介" + diff --git a/features/announcement/impl/src/main/res/values-ko/translations.xml b/features/announcement/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..3fbf2b953c --- /dev/null +++ b/features/announcement/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,11 @@ + + + "직접 만들거나 참여 중인 스페이스 보기" + "스페이스 초대 수락 또는 거절" + "참여 가능한 스페이스 내 모든 방 탐색" + "공개 스페이스 참여" + "참여 중인 스페이스 나가기" + "스페이스 필터링, 생성 및 관리 기능이 곧 추가될 예정입니다." + "스페이스 베타 버전에 오신 것을 환영합니다! 이번 첫 번째 버전에서는 다음과 같은 기능을 이용하실 수 있습니다.:" + "스페이스 소개" + diff --git a/features/announcement/impl/src/main/res/values-nb/translations.xml b/features/announcement/impl/src/main/res/values-nb/translations.xml new file mode 100644 index 0000000000..553ff9f997 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-nb/translations.xml @@ -0,0 +1,11 @@ + + + "Se områder du har opprettet eller blitt med i" + "Godta eller avslå invitasjoner til områder" + "Oppdag alle rom du kan bli med i i dine områder" + "Bli med i offentlige områder" + "Forlat områder du har blitt med i" + "Oppretting, filtrering og administrasjon av områder kommer snart." + "Velkommen til betaversjonen av Områder! Med denne første versjonen kan du:" + "Vi introduserer Områder" + diff --git a/features/announcement/impl/src/main/res/values-pl/translations.xml b/features/announcement/impl/src/main/res/values-pl/translations.xml new file mode 100644 index 0000000000..4308bdd81d --- /dev/null +++ b/features/announcement/impl/src/main/res/values-pl/translations.xml @@ -0,0 +1,11 @@ + + + "Wyświetlić przestrzenie, które stworzyłeś lub do których dołączyłeś" + "Akceptować lub odrzucać zaproszenia" + "Odkrywać wszystkie pokoje, do których możesz dołączyć w swoich przestrzeniach" + "Dołączać do przestrzeni publicznych" + "Opuszczać jakąkolwiek przestrzeń, do której dołączyłeś" + "Filtrowanie, tworzenie i zarządzanie przestrzeniami pojawi się wkrótce." + "Witamy w wersji beta przestrzeni! W tej wersji możesz:" + "Przedstawiamy przestrzenie" + diff --git a/features/announcement/impl/src/main/res/values-pt-rBR/translations.xml b/features/announcement/impl/src/main/res/values-pt-rBR/translations.xml new file mode 100644 index 0000000000..32a9bf85af --- /dev/null +++ b/features/announcement/impl/src/main/res/values-pt-rBR/translations.xml @@ -0,0 +1,11 @@ + + + "Visualizar espaços que criou ou entrou" + "Aceitar ou recusar convites aos espaços" + "Descobrir quaisquer salas que você pode entrar nos espaços" + "Entrar espaços públicos" + "Sair de quaisquer espaços que entrou" + "Filtrar, criar, e gerenciar espaços virão em breve." + "Boas-vindas à versão beta dos Espaços! Com essa primeira versão, você pode:" + "Apresentando Espaços" + diff --git a/features/announcement/impl/src/main/res/values-pt/translations.xml b/features/announcement/impl/src/main/res/values-pt/translations.xml new file mode 100644 index 0000000000..744ac74bd3 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-pt/translations.xml @@ -0,0 +1,11 @@ + + + "Ver espaços que criaste ou nos quais entraste" + "Aceitar ou recusar convites para espaços" + "Descobrir todas as salas dos seus espaços nas quais podes entrar" + "Entrar em espaços públicos" + "Deixar todos os espaços em que entraste" + "Em breve, será possível filtrar, criar e gerir espaços." + "Eis a versão beta dos Espaços! Nesta primeira versão, podes:" + "Apresentamos os Espaços" + diff --git a/features/announcement/impl/src/main/res/values-ro/translations.xml b/features/announcement/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..716f1faeb2 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,11 @@ + + + "Vizualizați spațiile pe care le-ați creat sau la care v-ați alăturat" + "Acceptați sau refuzați invitațiile la spații" + "Descoperiți toate camerele la care vă puteți alătura în spațiile dumneavoastră." + "Alăturați-vă spațiilor publice" + "Părăsiți spațiile la care v-ați alăturat." + "Filtrarea, crearea și gestionarea spațiilor vor fi disponibile în curând." + "Bun venit la versiunea beta a Spațiilor! Cu această primă versiune puteți:" + "Vă prezentăm Spații" + diff --git a/features/announcement/impl/src/main/res/values-ru/translations.xml b/features/announcement/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..46d005c8cd --- /dev/null +++ b/features/announcement/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,11 @@ + + + "Просматривать пространства, которые вы создали или к которым присоединились" + "Принимать или отклонять приглашения в пространства" + "Находить все комнаты, к которым можно присоединиться в ваших пространствах" + "Присоединяться к публичным пространствам" + "Покидать все пространства, к которым вы присоединились" + "Фильтровать, создавать пространства и управлять ими можно будет позже." + "Добро пожаловать в бета-версию пространств! Сейчас вы сможете:" + "Представляем пространства" + diff --git a/features/announcement/impl/src/main/res/values-sk/translations.xml b/features/announcement/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..0b305499a7 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,11 @@ + + + "Zobraziť priestory, ktoré ste vytvorili alebo ku ktorým ste sa pripojili" + "Prijímať alebo odmietať pozvánky do priestorov" + "Objaviť všetky miestnosti, do ktorých sa môžete pripojiť vo svojich priestoroch" + "Pripojiť sa k verejnému priestoru" + "Opustiť akékoľvek priestory, ku ktorým ste sa pridali" + "Filtrovanie, vytváranie a správa priestorov bude čoskoro k dispozícii." + "Vitajte v beta verzii priestorov! S touto prvou verziou môžete:" + "Predstavujeme priestory" + diff --git a/features/announcement/impl/src/main/res/values-tr/translations.xml b/features/announcement/impl/src/main/res/values-tr/translations.xml new file mode 100644 index 0000000000..8551dbb02b --- /dev/null +++ b/features/announcement/impl/src/main/res/values-tr/translations.xml @@ -0,0 +1,11 @@ + + + "Oluşturduğunuz veya katıldığınız alanları görüntüleyin" + "Alan davetlerini kabul edin veya reddedin" + "Alanlarınızdaki katılabileceğiniz odaları keşfedin" + "Herkese açık alanlara katılın" + "Katıldığınız alanlardan ayrılın" + "Alanları filtreleme, oluşturma ve yönetme yakında geliyor." + "Alanlar’ın beta sürümüne hoş geldiniz! Bu ilk sürümle şunları yapabilirsiniz:" + "Alanlar ile tanışın" + diff --git a/features/announcement/impl/src/main/res/values-uk/translations.xml b/features/announcement/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..de3c1b0324 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,7 @@ + + + "Знаходьте у своїх просторах кімнати, до яких можна приєднатися" + "Фільтрування, створення та керування просторами стане доступним найближчим часом." + "Ласкаво просимо до бета-версії Просторів! У цій першій версії ви можете:" + "Представляємо Простори" + diff --git a/features/announcement/impl/src/main/res/values-uz/translations.xml b/features/announcement/impl/src/main/res/values-uz/translations.xml new file mode 100644 index 0000000000..12356160b8 --- /dev/null +++ b/features/announcement/impl/src/main/res/values-uz/translations.xml @@ -0,0 +1,11 @@ + + + "Siz yaratgan yoki qo‘shilgan maydonlarni ko‘rish" + "Maydonlarga takliflarni qabul qilish yoki rad etish" + "Maydonlaringizga qo‘shilishingiz mumkin bo‘lgan xonalarni kashf eting" + "Jamoat maydonlariga qo‘shilish" + "Kirgan maydonlaringizni tark eting" + "Maydonlarni filtrlash, yaratish va boshqarish tez orada amalga oshiriladi." + "Maydonlar beta versiyasiga xush kelibsiz! Bu birinchi versiya bilan siz:" + "Maydonlar bilan tanishish" + diff --git a/features/announcement/impl/src/main/res/values-zh-rTW/translations.xml b/features/announcement/impl/src/main/res/values-zh-rTW/translations.xml new file mode 100644 index 0000000000..a5b82752bc --- /dev/null +++ b/features/announcement/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,11 @@ + + + "檢視您建立或加入的空間" + "接受或拒絕空間邀請" + "探索空間內您可以加入的任何聊天室" + "加入公開空間" + "離開任何您已加入的空間" + "篩選、建立與管理空間功能即將推出。" + "歡迎使用空間的測試版!此初始版本可讓您:" + "介紹空間" + diff --git a/features/announcement/impl/src/main/res/values-zh/translations.xml b/features/announcement/impl/src/main/res/values-zh/translations.xml new file mode 100644 index 0000000000..70d86638ea --- /dev/null +++ b/features/announcement/impl/src/main/res/values-zh/translations.xml @@ -0,0 +1,11 @@ + + + "查看您创建或加入的空间" + "接受或拒绝空间邀请" + "发现您可以加入空间的所有房间" + "加入公共空间" + "离开你加入的所有空间" + "筛选、创建及管理空间功能即将上线。" + "欢迎使用空间测试版!使用首个版本,您可以:" + "空间简介" + diff --git a/features/announcement/impl/src/main/res/values/localazy.xml b/features/announcement/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..5e7b8a6713 --- /dev/null +++ b/features/announcement/impl/src/main/res/values/localazy.xml @@ -0,0 +1,11 @@ + + + "View spaces you\'ve created or joined" + "Accept or decline invites to spaces" + "Discover any rooms you can join in your spaces" + "Join public spaces" + "Leave any spaces you’ve joined" + "Filtering, creating and managing spaces is coming soon." + "Welcome to the beta version of Spaces! With this first version you can:" + "Introducing Spaces" + 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 b7932898a8..b69037e61a 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.impl.AnnouncementEvent @@ -23,39 +20,43 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class FullscreenAnnouncementViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on back sends a AnnouncementEvent`() = runAndroidComposeUiTest { + fun `clicking on back sends a AnnouncementEvent`() { val eventsRecorder = EventsRecorder() - setFullscreenAnnouncementView( + rule.setFullscreenAnnouncementView( anAnnouncementState( announcement = Announcement.Fullscreen.Space, eventSink = eventsRecorder, ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) } @Test - fun `clicking on Continue sends a AnnouncementEvent`() = runAndroidComposeUiTest { + fun `clicking on Continue sends a AnnouncementEvent`() { val eventsRecorder = EventsRecorder() - setFullscreenAnnouncementView( + rule.setFullscreenAnnouncementView( anAnnouncementState( announcement = Announcement.Fullscreen.Space, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) } } -private fun AndroidComposeUiTest.setFullscreenAnnouncementView( +private fun AndroidComposeTestRule.setFullscreenAnnouncementView( state: AnnouncementState, ) { setContent { diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallData.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt similarity index 50% rename from features/call/api/src/main/kotlin/io/element/android/features/call/api/CallData.kt rename to features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt index c1dcf573c6..4b09813418 100644 --- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallData.kt +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt @@ -14,9 +14,22 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.parcelize.Parcelize -@Parcelize -data class CallData( - val sessionId: SessionId, - val roomId: RoomId, - val isAudioCall: Boolean -) : NodeInputs, Parcelable +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)" + } + } +} 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 2976635ee2..caa557f4de 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 callData The data of call to start. + * @param callType The type of call to start. */ - fun startCall(callData: CallData) + fun startCall(callType: CallType) /** * Handle an incoming call. - * @param callData The data of call. + * @param callType The type 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( - callData: CallData, + callType: CallType.RoomCall, eventId: EventId, senderId: UserId, roomName: String?, diff --git a/features/call/impl/src/main/AndroidManifest.xml b/features/call/impl/src/main/AndroidManifest.xml index c35c6843ff..daf1a910c9 100644 --- a/features/call/impl/src/main/AndroidManifest.xml +++ b/features/call/impl/src/main/AndroidManifest.xml @@ -30,10 +30,44 @@ + android:taskAffinity="io.element.android.features.call"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (null) } - fun handleEvent(event: PictureInPictureEvent) { + fun handleEvent(event: PictureInPictureEvents) { when (event) { - is PictureInPictureEvent.SetPipController -> { + is PictureInPictureEvents.SetPipController -> { pipController = event.pipController } - PictureInPictureEvent.EnterPictureInPicture -> { + PictureInPictureEvents.EnterPictureInPicture -> { coroutineScope.launch { switchToPip(pipController) } } - is PictureInPictureEvent.OnPictureInPictureModeChanged -> { + is PictureInPictureEvents.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 108589edb9..b1fef4f28b 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: (PictureInPictureEvent) -> Unit, + val eventSink: (PictureInPictureEvents) -> 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 f4a78294b6..6324820eec 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: (PictureInPictureEvent) -> Unit = {}, + eventSink: (PictureInPictureEvents) -> 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 bf27e8d39d..179e6c2b22 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.CallData +import io.element.android.features.call.api.CallType 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( - callData = CallData( + callType = CallType.RoomCall( 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 9e551b3e1b..3a51a014df 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,8 +9,6 @@ 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 @@ -36,8 +34,8 @@ internal fun aCallNotificationData( roomId = RoomId("!1234:matrix.org"), eventId = EventId("\$asdadadsad:matrix.org"), senderId = UserId("@bob:matrix.org"), - roomName = ROOM_NAME, - senderName = USER_NAME_BOB, + roomName = "A room", + senderName = "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 deleted file mode 100644 index cd47cd8bb1..0000000000 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenBackPressPolicy.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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/CallScreenEvent.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt similarity index 78% rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvent.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt index 357559c3f9..8fbbce896f 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvent.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt @@ -10,8 +10,8 @@ package io.element.android.features.call.impl.ui import io.element.android.features.call.impl.utils.WidgetMessageInterceptor -sealed interface CallScreenEvent { - data object Hangup : CallScreenEvent - data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvent - data class OnWebViewError(val description: String?) : CallScreenEvent +sealed interface CallScreenEvents { + data object Hangup : CallScreenEvents + data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents + data class OnWebViewError(val description: String?) : CallScreenEvents } 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 7d8e20967f..da2c57c0ac 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.CallData +import io.element.android.features.call.api.CallType 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 callData: CallData, + @Assisted private val callType: CallType, @Assisted private val navigator: CallScreenNavigator, private val callWidgetProvider: CallWidgetProvider, userAgentProvider: UserAgentProvider, @@ -69,9 +69,10 @@ class CallScreenPresenter( ) : Presenter { @AssistedFactory interface Factory { - fun create(callData: CallData, navigator: CallScreenNavigator): CallScreenPresenter + fun create(callType: CallType, navigator: CallScreenNavigator): CallScreenPresenter } + private val isInWidgetMode = callType is CallType.RoomCall private val userAgent = userAgentProvider.provide() @Composable @@ -89,9 +90,9 @@ class CallScreenPresenter( DisposableEffect(Unit) { coroutineScope.launch { // Sets the call as joined - activeCallManager.joinedCall(callData) + activeCallManager.joinedCall(callType) fetchRoomCallUrl( - callData = callData, + inputs = callType, urlState = urlState, callWidgetDriver = callWidgetDriver, languageTag = languageTag, @@ -99,10 +100,19 @@ class CallScreenPresenter( ) } onDispose { - appCoroutineScope.launch { activeCallManager.hangUpCall(callData) } + appCoroutineScope.launch { activeCallManager.hangUpCall(callType) } } } - screenTracker.TrackScreen(screen = MobileScreen.ScreenName.RoomCall) + + when (callType) { + is CallType.ExternalUrl -> { + // No analytics yet for external calls + } + is CallType.RoomCall -> { + screenTracker.TrackScreen(screen = MobileScreen.ScreenName.RoomCall) + } + } + HandleMatrixClientSyncState() callWidgetDriver.value?.let { driver -> @@ -139,22 +149,25 @@ class CallScreenPresenter( .launchIn(this) } - LaunchedEffect(Unit) { - // Wait for the call to be joined, if it takes too long, we display an error - delay(10.seconds) + 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) - 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: CallScreenEvent) { + fun handleEvent(event: CallScreenEvents) { when (event) { - is CallScreenEvent.Hangup -> { + is CallScreenEvents.Hangup -> { val widgetId = callWidgetDriver.value?.id val interceptor = messageInterceptor.value if (widgetId != null && interceptor != null && isWidgetLoaded) { @@ -174,10 +187,10 @@ class CallScreenPresenter( } } } - is CallScreenEvent.SetupMessageChannels -> { + is CallScreenEvents.SetupMessageChannels -> { messageInterceptor.value = event.widgetMessageInterceptor } - is CallScreenEvent.OnWebViewError -> { + is CallScreenEvents.OnWebViewError -> { if (!ignoreWebViewError) { webViewError = event.description.orEmpty() } @@ -191,29 +204,37 @@ class CallScreenPresenter( webViewError = webViewError, userAgent = userAgent, isCallActive = isWidgetLoaded, + isInWidgetMode = isInWidgetMode, eventSink = ::handleEvent, ) } private suspend fun fetchRoomCallUrl( - callData: CallData, + inputs: CallType, urlState: MutableState>, callWidgetDriver: MutableState, languageTag: String?, theme: String?, ) { urlState.runCatchingUpdatingState { - 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 + 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 + } + } } } @@ -221,11 +242,12 @@ class CallScreenPresenter( private fun HandleMatrixClientSyncState() { val coroutineScope = rememberCoroutineScope() DisposableEffect(Unit) { - val client = matrixClientsProvider.getOrNull(callData.sessionId) ?: return@DisposableEffect onDispose { - Timber.w("No MatrixClient found for sessionId, can't send call notification: ${callData.sessionId}") + 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}") } coroutineScope.launch { - Timber.d("Observing sync state in-call for sessionId: ${callData.sessionId}") + Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}") client.syncService.syncState .collect { state -> if (state != SyncState.Running) { @@ -234,7 +256,7 @@ class CallScreenPresenter( } } onDispose { - Timber.d("Stopped observing sync state in-call for sessionId: ${callData.sessionId}") + Timber.d("Stopped observing sync state in-call for sessionId: ${roomCallType.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 86b4cc439f..c07594aebb 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,5 +15,6 @@ data class CallScreenState( val webViewError: String?, val userAgent: String, val isCallActive: Boolean, - val eventSink: (CallScreenEvent) -> Unit, + val isInWidgetMode: Boolean, + val eventSink: (CallScreenEvents) -> 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 155c5d3380..3e72f96f87 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,13 +26,15 @@ internal fun aCallScreenState( webViewError: String? = null, userAgent: String = "", isCallActive: Boolean = true, - eventSink: (CallScreenEvent) -> Unit = {}, + isInWidgetMode: Boolean = false, + eventSink: (CallScreenEvents) -> 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 2537eb739c..f8657a9ece 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,10 +17,9 @@ 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.systemBars +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -34,7 +33,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.PictureInPictureEvent +import io.element.android.features.call.impl.pip.PictureInPictureEvents 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 @@ -46,6 +45,7 @@ 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,93 +64,94 @@ internal fun CallScreenView( requestPermissions: (Array, RequestPermissionCallback) -> Unit, modifier: Modifier = Modifier, ) { - 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") + fun handleBack() { + if (pipState.supportPip) { + pipState.eventSink.invoke(PictureInPictureEvents.EnterPictureInPicture) + } else { + state.eventSink(CallScreenEvents.Hangup) } } - 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 - } + Scaffold( + modifier = modifier, + ) { padding -> + BackHandler { + handleBack() } + 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() - 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() + var invalidAudioDeviceReason by remember { mutableStateOf(null) } + invalidAudioDeviceReason?.let { + InvalidAudioDeviceDialog(invalidAudioDeviceReason = it) { + invalidAudioDeviceReason = null + } } - ) - 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) }, - ) + + 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 } - is AsyncData.Success -> Unit } } } @@ -247,16 +248,15 @@ private fun WebView.setup( private fun WebView.addBackHandler(onBackPressed: () -> Unit) { addJavascriptInterface( - JavascriptBackHandlerBridge(callback = onBackPressed), + object { + @Suppress("unused") + @JavascriptInterface + fun onBackPressed() = 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,12 +275,3 @@ 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 new file mode 100644 index 0000000000..0c18c3e1a4 --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt @@ -0,0 +1,19 @@ +/* + * 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 26df7c160c..bf4f836294 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,20 +32,19 @@ 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.CallData +import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallType.ExternalUrl 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.PictureInPictureEvent +import io.element.android.features.call.impl.pip.PictureInPictureEvents 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 @@ -55,8 +54,6 @@ 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 @@ -66,9 +63,9 @@ 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 @Inject lateinit var enterpriseService: EnterpriseService @Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter @Inject lateinit var buildMeta: BuildMeta @@ -81,9 +78,9 @@ class ElementCallActivity : private val requestPermissionsLauncher = registerPermissionResultLauncher() - private val webViewTarget = mutableStateOf(null) + private val webViewTarget = mutableStateOf(null) - private var eventSink: ((CallScreenEvent) -> Unit)? = null + private var eventSink: ((CallScreenEvents) -> Unit)? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -99,7 +96,7 @@ class ElementCallActivity : window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) } - setCallData(intent) + setCallType(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 @@ -112,41 +109,20 @@ class ElementCallActivity : setContent { val pipState = pictureInPicturePresenter.present() ListenToAndroidEvents(pipState) - val colors by remember(webViewTarget.value?.sessionId) { - enterpriseService.semanticColorsFlow(sessionId = webViewTarget.value?.sessionId) + val colors by remember(webViewTarget.value?.getSessionId()) { + enterpriseService.semanticColorsFlow(sessionId = webViewTarget.value?.getSessionId()) }.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, compoundLight = colors.light, compoundDark = colors.dark, buildMeta = buildMeta, ) { val state = presenter.present() eventSink = state.eventSink - LaunchedEffect(state.isCallActive) { - if (state.isCallActive) { + 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) { setCallIsActive() } } @@ -184,7 +160,7 @@ class ElementCallActivity : if (requestPermissionCallback != null) { Timber.tag(loggerTag.value).w("Ignoring onUserLeaveHint event because user is asked to grant permissions") } else { - pipEventSink(PictureInPictureEvent.EnterPictureInPicture) + pipEventSink(PictureInPictureEvents.EnterPictureInPicture) } } addOnUserLeaveHintListener(listener) @@ -194,10 +170,10 @@ class ElementCallActivity : } DisposableEffect(Unit) { val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo -> - pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode)) + pipEventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(isInPictureInPictureMode)) if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call") - eventSink?.invoke(CallScreenEvent.Hangup) + eventSink?.invoke(CallScreenEvents.Hangup) } } addOnPictureInPictureModeChangedListener(onPictureInPictureModeChangedListener) @@ -209,7 +185,7 @@ class ElementCallActivity : override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - setCallData(intent) + setCallType(intent) } override fun onDestroy() { @@ -228,24 +204,25 @@ class ElementCallActivity : finish() } - private fun setCallData(intent: Intent?) { - val callData = intent?.let { - IntentCompat.getParcelableExtra(intent, DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, CallData::class.java) + 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) } - val currentCallData = webViewTarget.value - if (currentCallData == null) { - if (callData == null) { + val currentCallType = webViewTarget.value + if (currentCallType == null) { + if (callType == 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 = callData - presenter = presenterFactory.create(callData, this) + webViewTarget.value = callType + presenter = presenterFactory.create(callType, this) } } else { - if (callData == null) { + if (callType == null) { Timber.tag(loggerTag.value).d("Coming back from notification, do nothing") - } else if (callData != currentCallData) { + } else if (callType != currentCallType) { Timber.tag(loggerTag.value).d("User starts another call, restart the Activity") setIntent(intent) recreate() @@ -256,6 +233,8 @@ class ElementCallActivity : } } + private fun parseUrl(url: String?): String? = callIntentDataParser.parse(url) + private fun registerPermissionResultLauncher(): ActivityResultLauncher> { return registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() @@ -305,7 +284,7 @@ class ElementCallActivity : } override fun hangUp() { - eventSink?.invoke(CallScreenEvent.Hangup) + eventSink?.invoke(CallScreenEvents.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 1d6989fb3c..73233fe453 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.CallData +import io.element.android.features.call.api.CallType 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 @@ -30,7 +30,6 @@ import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.designsystem.theme.ElementThemeApp import io.element.android.libraries.di.annotations.AppCoroutineScope -import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.preferences.api.store.AppPreferencesStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filter @@ -58,9 +57,6 @@ class IncomingCallActivity : AppCompatActivity() { @Inject lateinit var appPreferencesStore: AppPreferencesStore - @Inject - lateinit var featureFlagService: FeatureFlagService - @Inject lateinit var enterpriseService: EnterpriseService @@ -92,7 +88,6 @@ class IncomingCallActivity : AppCompatActivity() { }.collectAsState(SemanticColorsLightDark.default) ElementThemeApp( appPreferencesStore = appPreferencesStore, - featureFlagService = featureFlagService, compoundLight = colors.light, compoundDark = colors.dark, buildMeta = buildMeta, @@ -118,10 +113,10 @@ class IncomingCallActivity : AppCompatActivity() { private fun onAnswer(notificationData: CallNotificationData) { elementCallEntryPoint.startCall( - CallData( - sessionId = notificationData.sessionId, - roomId = notificationData.roomId, - isAudioCall = notificationData.audioOnly, + CallType.RoomCall( + notificationData.sessionId, + notificationData.roomId, + isAudioCall = notificationData.audioOnly ) ) } @@ -129,7 +124,7 @@ class IncomingCallActivity : AppCompatActivity() { private fun onCancel() { val activeCall = activeCallManager.activeCall.value ?: return appCoroutineScope.launch { - activeCallManager.hangUpCall(callData = activeCall.callData) + activeCallManager.hangUpCall(callType = activeCall.callType) } } } 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 685fc932fe..99679a8afb 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.CallData +import io.element.android.features.call.api.CallType 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 callData The data about the call. + * @param callType The type of call that the user hangs up, either an external url one or a room one. * @param notificationData The data for the incoming call notification. */ suspend fun hangUpCall( - callData: CallData, + callType: CallType, 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 callData The data about the call. + * @param callType The type of call that the user joined, either an external url one or a room one. */ - suspend fun joinedCall(callData: CallData) + suspend fun joinedCall(callType: CallType) } @SingleIn(AppScope::class) @@ -143,7 +143,7 @@ class DefaultActiveCallManager( return } activeCall.value = ActiveCall( - callData = CallData( + callType = CallType.RoomCall( sessionId = notificationData.sessionId, roomId = notificationData.roomId, isAudioCall = notificationData.audioOnly, @@ -198,17 +198,17 @@ class DefaultActiveCallManager( } override suspend fun hangUpCall( - callData: CallData, + callType: CallType, notificationData: CallNotificationData?, ) = mutex.withLock { - Timber.tag(tag).d("Hang up call: $callData") + Timber.tag(tag).d("Hang up call: $callType") 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( - callData = callData, + callType = callType, callState = CallState.Ringing( notificationData = notificationData, ) @@ -219,8 +219,8 @@ class DefaultActiveCallManager( return@withLock } - if (currentActiveCall.callData != callData) { - Timber.tag(tag).w("Call type $callData does not match the active call type, ignoring") + if (currentActiveCall.callType != callType) { + Timber.tag(tag).w("Call type $callType 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(callData: CallData) = mutex.withLock { - Timber.tag(tag).d("Joined call: $callData") + override suspend fun joinedCall(callType: CallType) = mutex.withLock { + Timber.tag(tag).d("Joined call: $callType") 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( - callData = callData, + callType = callType, callState = CallState.InCall, ) } @@ -307,15 +307,15 @@ class DefaultActiveCallManager( private fun observeRingingCall() { activeCall .filterNotNull() - .filter { it.callState is CallState.Ringing } + .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } .flatMapLatest { activeCall -> - val callData = activeCall.callData + val callType = activeCall.callType as CallType.RoomCall val ringingInfo = activeCall.callState as CallState.Ringing - val client = matrixClientProvider.getOrRestore(callData.sessionId).getOrNull() ?: run { + val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run { Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall") return@flatMapLatest flowOf() } - val room = client.getRoom(callData.roomId) ?: run { + val room = client.getRoom(callType.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 } + .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } .flatMapLatest { activeCall -> - val callData = activeCall.callData + val callType = activeCall.callType as CallType.RoomCall // Get a flow of updated `hasRoomCall` and `activeRoomCallParticipants` values for the room - val room = matrixClientProvider.getOrRestore(callData.sessionId).getOrNull()?.getRoom(callData.roomId) ?: run { + val room = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull()?.getRoom(callType.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 (callData.sessionId in it.activeRoomCallParticipants) + it.hasRoomCall to (callType.sessionId in it.activeRoomCallParticipants) } } // We only want to check if the room active call status changes @@ -388,7 +388,10 @@ class DefaultActiveCallManager( // Nothing to do } is CallState.InCall -> { - defaultCurrentCallService.onCallStarted(CurrentCall.RoomCall(value.callData.roomId)) + when (val callType = value.callType) { + is CallType.ExternalUrl -> defaultCurrentCallService.onCallStarted(CurrentCall.ExternalUrl(callType.url)) + is CallType.RoomCall -> defaultCurrentCallService.onCallStarted(CurrentCall.RoomCall(callType.roomId)) + } } } } @@ -401,7 +404,7 @@ class DefaultActiveCallManager( * Represents an active call. */ data class ActiveCall( - val callData: CallData, + val callType: CallType, 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 new file mode 100644 index 0000000000..f5433c15a0 --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt @@ -0,0 +1,98 @@ +/* + * 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 8de7b81d6d..b31b6152d0 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,6 +14,7 @@ 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 c6c607cbbc..0f74ba86d4 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.CallData +import io.element.android.features.call.api.CallType import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.ui.ElementCallActivity internal object IntentProvider { - fun createIntent(context: Context, callData: CallData): Intent = Intent(context, ElementCallActivity::class.java).apply { - putExtra(DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, callData) + fun createIntent(context: Context, callType: CallType): Intent = Intent(context, ElementCallActivity::class.java).apply { + putExtra(DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, callType) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION) } - fun getPendingIntent(context: Context, callData: CallData): PendingIntent { + fun getPendingIntent(context: Context, callType: CallType): PendingIntent { return PendingIntentCompat.getActivity( context, DefaultElementCallEntryPoint.REQUEST_CODE, - createIntent(context, callData), + createIntent(context, callType), 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 febd919149..0c1ecf83cb 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 @@ -64,11 +64,6 @@ class WebViewAudioManager( */ private val isWebViewAudioEnabled = AtomicBoolean(true) - /** - * Store the device id requested by EC, and re-set it if something try to switch (only android S+). - */ - private var ecRequestedDeviceId: String? = null - /** * The list of device types that are considered as communication devices, sorted by likelihood of it being used for communication. */ @@ -118,12 +113,22 @@ class WebViewAudioManager( @get:RequiresApi(Build.VERSION_CODES.S) private val commsDeviceChangedListener by lazy { AudioManager.OnCommunicationDeviceChangedListener { device -> - Timber.d("Audio device changed, type: ${device?.id}") - val wantedDevice = this.ecRequestedDeviceId - if (wantedDevice != null && this.ecRequestedDeviceId != device?.id?.toString()) { - // We want to ensure that we stick to what EC selected even if it was changed outside - Timber.d("Audio device changed to unwanted device ${device?.id}, enforce using the expected device $wantedDevice") - audioManager.selectAudioDevice(wantedDevice) + if (device != null && device.id == expectedNewCommunicationDeviceId) { + expectedNewCommunicationDeviceId = null + Timber.d("Audio device changed, type: ${device.type}") + updateSelectedAudioDeviceInWebView(device.id.toString()) + } else if (device != null && device.id != expectedNewCommunicationDeviceId) { + // We were expecting a device change but it didn't happen, so we should retry + val expectedDeviceId = expectedNewCommunicationDeviceId + if (expectedDeviceId != null) { + // Remove the expected id so we only retry once + expectedNewCommunicationDeviceId = null + audioManager.selectAudioDevice(expectedDeviceId.toString()) + } + } else { + Timber.d("Audio device cleared") + expectedNewCommunicationDeviceId = null + audioManager.selectAudioDevice(null) } } } @@ -139,15 +144,40 @@ class WebViewAudioManager( // We need to calculate the available devices ourselves, since calling `listAudioDevices` will return an outdated list val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }.sortedWith(audioDeviceComparator) setAvailableAudioDevices(audioDevices.map(SerializableAudioDevice::fromAudioDeviceInfo)) + // This should automatically switch to a new device if it has a higher priority than the current one + selectDefaultAudioDevice(audioDevices) } override fun onAudioDevicesRemoved(removedDevices: Array?) { // Update the available devices - // Element Call will then decide to switch devices if needed setAvailableAudioDevices() + + // Unless the removed device is the current one, we don't need to do anything else + val removedCurrentDevice = removedDevices.orEmpty().any { it.id == currentDeviceId } + if (!removedCurrentDevice) return + + val previousDevice = previousSelectedDevice + if (previousDevice != null) { + previousSelectedDevice = null + // If we have a previous device, we should select it again + audioManager.selectAudioDevice(previousDevice.id.toString()) + } else { + // If we don't have a previous device, we should select the default one + selectDefaultAudioDevice() + } } } + /** + * The currently used audio device id. + */ + private var currentDeviceId: Int? = null + + /** + * When a new audio device is selected but not yet set as the communication device by the OS, this id is used to check if the device is the expected one. + */ + private var expectedNewCommunicationDeviceId: Int? = null + /** * Previously selected device, used to restore the selection when the selected device is removed. */ @@ -201,9 +231,12 @@ class WebViewAudioManager( return } - // 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() + coroutineScope.launch { + proximitySensorMutex.withLock { + if (proximitySensorWakeLock?.isHeld == true) { + proximitySensorWakeLock?.release() + } + } } audioManager.mode = AudioManager.MODE_NORMAL @@ -230,7 +263,6 @@ class WebViewAudioManager( val webViewAudioDeviceSelectedCallback = AndroidWebViewAudioBridge( onAudioDeviceSelected = { selectedDeviceId -> previousSelectedDevice = listAudioDevices().find { it.id.toString() == selectedDeviceId } - this.ecRequestedDeviceId = selectedDeviceId audioManager.selectAudioDevice(selectedDeviceId) }, onAudioPlaybackStarted = { @@ -298,6 +330,34 @@ class WebViewAudioManager( }) } + /** + * Selects the default audio device based on the sorted available devices. + * + * @param availableDevices The list of available audio devices to select from. If not provided, it will use the current list of audio devices. + */ + private fun selectDefaultAudioDevice(availableDevices: List = listAudioDevices()) { + val selectedDevice = availableDevices.firstOrNull() + expectedNewCommunicationDeviceId = selectedDevice?.id + audioManager.selectAudioDevice(selectedDevice) + + selectedDevice?.let { + updateSelectedAudioDeviceInWebView(it.id.toString()) + } ?: run { + Timber.w("Audio: unable to select default audio device") + } + } + + /** + * Updates the WebView's UI to reflect the selected audio device. + * + * @param deviceId The id of the selected audio device. + */ + private fun updateSelectedAudioDeviceInWebView(deviceId: String) { + coroutineScope.launch(Dispatchers.Main) { + webView.evaluateJavascript("controls.setOutputDevice('$deviceId');", null) + } + } + /** * Selects the audio device on the OS based on the provided device id. * @@ -321,14 +381,14 @@ class WebViewAudioManager( * * @param device The info of the audio device to select, or none to clear the selected device. */ + @Suppress("DEPRECATION") private fun AudioManager.selectAudioDevice(device: AudioDeviceInfo?) { + currentDeviceId = device?.id if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (device != null) { runCatchingExceptions { Timber.d("Setting communication device: ${device.id} - ${deviceName(device.type, device.productName.toString())}") - if (!setCommunicationDevice(device)) { - Timber.w("Failed to setCommunication device") - } + setCommunicationDevice(device) }.onFailure { Timber.e(it, "Could not set communication device.") } @@ -350,24 +410,22 @@ 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 } } + expectedNewCommunicationDeviceId = null + coroutineScope.launch { proximitySensorMutex.withLock { @Suppress("WakeLock", "WakeLockTimeout") - if (device?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { - if (proximitySensorWakeLock?.isHeld == false) { - // If the device is the built-in earpiece, we need to acquire the proximity sensor wake lock - proximitySensorWakeLock?.acquire() - } + if (device?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && proximitySensorWakeLock?.isHeld == false) { + // If the device is the built-in earpiece, we need to acquire the proximity sensor wake lock + proximitySensorWakeLock?.acquire() } else if (proximitySensorWakeLock?.isHeld == true) { // If the device is no longer the earpiece, we need to release the wake lock proximitySensorWakeLock?.release() 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 c74ae90abd..f7ab2c57af 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,33 +140,26 @@ class WebViewWidgetMessageInterceptor( } } - // 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) + // Create a WebMessageListener, which will receive messages from the WebView and reply to them + val webMessageListener = WebViewCompat.WebMessageListener { _, message, _, _, _ -> + onMessageReceived(message.data) + } - // 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)) { + // Use WebMessageListener if supported, otherwise use JavascriptInterface + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { WebViewCompat.addWebMessageListener( webView, LISTENER_NAME, setOf("*"), - WebViewCompat.WebMessageListener { _, message, _, _, _ -> - onMessageReceived(message.data) - } + webMessageListener ) + } else { + webView.addJavascriptInterface(object { + @JavascriptInterface + fun postMessage(json: String?) { + onMessageReceived(json) + } + }, 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 deleted file mode 100644 index 92c518dbee..0000000000 --- a/features/call/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - "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 fa0415a5f5..16b72b8b97 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." - "Saabuv Element Calli kõne" + "Sissetulev Element Calli kõne" diff --git a/features/call/impl/src/main/res/values-zh/translations.xml b/features/call/impl/src/main/res/values-zh/translations.xml index 7a81fde819..6192568a61 100644 --- a/features/call/impl/src/main/res/values-zh/translations.xml +++ b/features/call/impl/src/main/res/values-zh/translations.xml @@ -1,8 +1,8 @@ "通话进行中" - "点击以返回通话" + "点按即可返回通话" "☎️ 通话中" - "Element Call 不支持在此 Android 版本中使用蓝牙音频设备。请选择其它音频设备。" - "Element Call 来电" + "Element Call 不支持在此 Android 版本中使用蓝牙音频设备。请选择其他音频设备。" + "Element 来电" 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 f21447cc85..85cec8c586 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.CallData +import io.element.android.features.call.api.CallType 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 @@ -37,7 +37,7 @@ class DefaultElementCallEntryPointTest { @Test fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest { val entryPoint = createEntryPoint() - entryPoint.startCall(CallData(A_SESSION_ID, A_ROOM_ID, isAudioCall = false)) + entryPoint.startCall(CallType.RoomCall(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 +53,7 @@ class DefaultElementCallEntryPointTest { val entryPoint = createEntryPoint(activeCallManager = activeCallManager) entryPoint.handleIncomingCall( - callData = CallData(A_SESSION_ID, A_ROOM_ID, isAudioCall = false), + callType = CallType.RoomCall(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 c3d7fdf17b..c087fa3c35 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,9 +8,11 @@ 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 @@ -18,7 +20,9 @@ class PictureInPicturePresenterTest { @Test fun `when pip is not supported, the state value supportPip is false`() = runTest { val presenter = createPictureInPicturePresenter(supportPip = false) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { val initialState = awaitItem() assertThat(initialState.supportPip).isFalse() } @@ -31,7 +35,9 @@ class PictureInPicturePresenterTest { supportPip = true, pipView = FakePipView(setPipParamsResult = { }), ) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { val initialState = awaitItem() assertThat(initialState.supportPip).isTrue() } @@ -47,16 +53,18 @@ class PictureInPicturePresenterTest { enterPipModeResult = enterPipModeResult, ), ) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { val initialState = awaitItem() assertThat(initialState.isInPictureInPicture).isFalse() - initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) + initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture) enterPipModeResult.assertions().isCalledOnce() - initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(true)) + initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(true)) val pipState = awaitItem() assertThat(pipState.isInPictureInPicture).isTrue() // User stops pip - initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(false)) + initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(false)) val finalState = awaitItem() assertThat(finalState.isInPictureInPicture).isFalse() } @@ -72,10 +80,12 @@ class PictureInPicturePresenterTest { handUpResult = handUpResult ), ) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { val initialState = awaitItem() - initialState.eventSink(PictureInPictureEvent.SetPipController(FakePipController(canEnterPipResult = { false }))) - initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) + initialState.eventSink(PictureInPictureEvents.SetPipController(FakePipController(canEnterPipResult = { false }))) + initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture) handUpResult.assertions().isCalledOnce() } } @@ -92,10 +102,12 @@ class PictureInPicturePresenterTest { enterPipModeResult = enterPipModeResult ), ) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { val initialState = awaitItem() initialState.eventSink( - PictureInPictureEvent.SetPipController( + PictureInPictureEvents.SetPipController( FakePipController( canEnterPipResult = { true }, enterPipResult = enterPipResult, @@ -103,16 +115,16 @@ class PictureInPicturePresenterTest { ) ) ) - initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) + initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture) enterPipModeResult.assertions().isCalledOnce() enterPipResult.assertions().isNeverCalled() - initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(true)) + initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(true)) val pipState = awaitItem() assertThat(pipState.isInPictureInPicture).isTrue() enterPipResult.assertions().isCalledOnce() // User stops pip exitPipResult.assertions().isNeverCalled() - initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(false)) + initialState.eventSink(PictureInPictureEvents.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/ui/CallDataTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallDataTest.kt deleted file mode 100644 index f0cdd44082..0000000000 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallDataTest.kt +++ /dev/null @@ -1,23 +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.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 deleted file mode 100644 index f07f7039d3..0000000000 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenBackPressPolicyTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 276e6670f1..b6b0120451 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.CallData -import io.element.android.features.call.impl.ui.CallScreenEvent +import io.element.android.features.call.api.CallType +import io.element.android.features.call.impl.ui.CallScreenEvents 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,7 +39,6 @@ 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 @@ -60,19 +59,46 @@ class CallScreenPresenterTest { val warmUpRule = WarmUpRule() @Test - fun `present - with CallData sets call as active, loads URL and runs WidgetDriver`() = runTest { + 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 { val widgetDriver = FakeMatrixWidgetDriver() val widgetProvider = FakeCallWidgetProvider(widgetDriver) val analyticsLambda = lambdaRecorder {} - val joinedCallLambda = lambdaRecorder {} + val joinedCallLambda = lambdaRecorder {} val presenter = createCallScreenPresenter( - callData = CallData(A_SESSION_ID, A_ROOM_ID, false), + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, widgetProvider = widgetProvider, screenTracker = FakeScreenTracker(analyticsLambda), activeCallManager = FakeActiveCallManager(joinedCallResult = joinedCallLambda), ) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { // Wait until the URL is loaded advanceTimeBy(1.seconds) skipItems(1) @@ -81,6 +107,7 @@ 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)) @@ -96,17 +123,19 @@ class CallScreenPresenterTest { fun `present - set message interceptor, send and receive messages`() = runTest { val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callData = CallData(A_SESSION_ID, A_ROOM_ID, false), + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) val initialState = awaitItem() - initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) // And incoming message from the Widget Driver is passed to the WebView widgetDriver.givenIncomingMessage("A message") @@ -125,22 +154,24 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callData = CallData(A_SESSION_ID, A_ROOM_ID, false), + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { val initialState = awaitItem() // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) - initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) - initialState.eventSink(CallScreenEvent.Hangup) + initialState.eventSink(CallScreenEvents.Hangup) // Let background coroutines run and the widget drive be received runCurrent() @@ -157,20 +188,22 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callData = CallData(A_SESSION_ID, A_ROOM_ID, false), + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { val initialState = awaitItem() // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) - initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) messageInterceptor.givenInterceptedMessage("""{"action":"io.element.close","api":"fromWidget","widgetId":"1","requestId":"1"}""") @@ -190,20 +223,22 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callData = CallData(A_SESSION_ID, A_ROOM_ID, false), + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) skipItems(2) val initialState = awaitItem() assertThat(initialState.isCallActive).isFalse() - initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) messageInterceptor.givenInterceptedMessage( """ { @@ -225,20 +260,22 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callData = CallData(A_SESSION_ID, A_ROOM_ID, false), + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) skipItems(2) val initialState = awaitItem() assertThat(initialState.isCallActive).isFalse() - initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) skipItems(2) // Wait for the timeout to trigger @@ -263,7 +300,7 @@ class CallScreenPresenterTest { val matrixClient = FakeMatrixClient(syncService = syncService) val appForegroundStateService = FakeAppForegroundStateService() val presenter = createCallScreenPresenter( - callData = CallData(A_SESSION_ID, A_ROOM_ID, false), + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), @@ -301,8 +338,53 @@ 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( - callData: CallData, + callType: CallType, navigator: CallScreenNavigator = FakeCallScreenNavigator(), widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(), widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver), @@ -319,7 +401,7 @@ class CallScreenPresenterTest { } val clock = SystemClock { 0 } return CallScreenPresenter( - callData = callData, + callType = callType, 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 deleted file mode 100644 index 99aaee6f39..0000000000 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 androidx.test.ext.junit.runners.AndroidJUnit4 -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 org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith -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) -@RunWith(AndroidJUnit4::class) -class CallScreenViewTest { - @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 new file mode 100644 index 0000000000..c83408bd3b --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt @@ -0,0 +1,45 @@ +/* + * 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 new file mode 100644 index 0000000000..43f7f931f1 --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt @@ -0,0 +1,226 @@ +/* + * 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 3712904b03..f9f6206ec7 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -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.CallData +import io.element.android.features.call.api.CallType import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator import io.element.android.features.call.impl.notifications.aCallNotificationData import io.element.android.features.call.impl.utils.ActiveCall @@ -77,7 +77,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callData = CallData( + callType = CallType.RoomCall( sessionId = callNotificationData.sessionId, roomId = callNotificationData.roomId, isAudioCall = false, @@ -104,7 +104,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callData = CallData( + callType = CallType.RoomCall( sessionId = callNotificationData.sessionId, roomId = callNotificationData.roomId, isAudioCall = true, @@ -132,7 +132,7 @@ class DefaultActiveCallManagerTest { manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2)) assertThat(manager.activeCall.value).isEqualTo(activeCall) - assertThat(manager.activeCall.value?.callData?.roomId).isNotEqualTo(A_ROOM_ID_2) + assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2) advanceTimeBy(1) @@ -178,7 +178,7 @@ class DefaultActiveCallManagerTest { } @Test - fun `hangUpCall - removes existing call if the CallData matches`() = runTest { + fun `hangUpCall - removes existing call if the CallType matches`() = runTest { setupShadowPowerManager() val notificationManagerCompat = mockk(relaxed = true) val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) @@ -188,7 +188,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeWakeLock?.isHeld).isTrue() - manager.hangUpCall(CallData(notificationData.sessionId, notificationData.roomId, false)) + manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false)) assertThat(manager.activeCall.value).isNull() assertThat(manager.activeWakeLock?.isHeld).isFalse() @@ -215,7 +215,7 @@ class DefaultActiveCallManagerTest { val notificationData = aCallNotificationData(roomId = A_ROOM_ID) manager.registerIncomingCall(notificationData) - manager.hangUpCall(CallData(notificationData.sessionId, notificationData.roomId, false)) + manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false)) coVerify { room.declineCall(notificationEventId = notificationData.eventId) @@ -242,7 +242,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( - callData = CallData(notificationData.sessionId, notificationData.roomId, false), + callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false), notificationData = notificationData, ) coVerify { @@ -320,7 +320,7 @@ class DefaultActiveCallManagerTest { } @Test - fun `hangUpCall - does nothing if the CallData doesn't match`() = runTest { + fun `hangUpCall - does nothing if the CallType doesn't match`() = runTest { setupShadowPowerManager() val notificationManagerCompat = mockk(relaxed = true) val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) @@ -329,13 +329,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeWakeLock?.isHeld).isTrue() - manager.hangUpCall( - CallData( - sessionId = A_SESSION_ID, - roomId = A_ROOM_ID_2, - isAudioCall = true, - ) - ) + manager.hangUpCall(CallType.ExternalUrl("https://example.com")) assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeWakeLock?.isHeld).isTrue() @@ -350,10 +344,10 @@ class DefaultActiveCallManagerTest { val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) assertThat(manager.activeCall.value).isNull() - manager.joinedCall(CallData(A_SESSION_ID, A_ROOM_ID, true)) + manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, true)) assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callData = CallData( + callType = CallType.RoomCall( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, isAudioCall = true, @@ -456,7 +450,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callData = CallData( + callType = CallType.RoomCall( 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 c2c38284a9..2d0e126ab5 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.CallData +import io.element.android.features.call.api.CallType 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: (CallData, CallNotificationData?) -> Unit = { _, _ -> }, - var joinedCallResult: (CallData) -> Unit = {}, + var hangUpCallResult: (CallType, CallNotificationData?) -> Unit = { _, _ -> }, + var joinedCallResult: (CallType) -> Unit = {}, ) : ActiveCallManager { override val activeCall = MutableStateFlow(null) @@ -26,12 +26,12 @@ class FakeActiveCallManager( registerIncomingCallResult(notificationData) } - override suspend fun hangUpCall(callData: CallData, notificationData: CallNotificationData?) = simulateLongTask { - hangUpCallResult(callData, notificationData) + override suspend fun hangUpCall(callType: CallType, notificationData: CallNotificationData?) = simulateLongTask { + hangUpCallResult(callType, notificationData) } - override suspend fun joinedCall(callData: CallData) = simulateLongTask { - joinedCallResult(callData) + override suspend fun joinedCall(callType: CallType) = simulateLongTask { + joinedCallResult(callType) } 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 13b61feacb..fdf3ca566b 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.CallData +import io.element.android.features.call.api.CallType 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: (CallData) -> Unit = { lambdaError() }, + var startCallResult: (CallType) -> Unit = { lambdaError() }, var handleIncomingCallResult: ( - CallData, + CallType.RoomCall, EventId, UserId, String?, @@ -27,12 +27,12 @@ class FakeElementCallEntryPoint( String?, ) -> Unit = { _, _, _, _, _, _, _, _ -> lambdaError() } ) : ElementCallEntryPoint { - override fun startCall(callData: CallData) { - startCallResult(callData) + override fun startCall(callType: CallType) { + startCallResult(callType) } override suspend fun handleIncomingCall( - callData: CallData, + callType: CallType.RoomCall, eventId: EventId, senderId: UserId, roomName: String?, @@ -44,7 +44,7 @@ class FakeElementCallEntryPoint( textContent: String?, ) { handleIncomingCallResult( - callData, + callType, eventId, senderId, roomName, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/SelectParentSpaceOptions.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/SelectParentSpaceOptions.kt index 1480e57334..6b7b66b897 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/SelectParentSpaceOptions.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/SelectParentSpaceOptions.kt @@ -95,8 +95,7 @@ internal fun SelectParentSpaceOptions( sheetState.hide(coroutineScope) { displaySelectSpaceBottomSheet = false } - }, - scrollable = false, + } ) { SelectParentSpaceBottomSheet( spaces = spaces, diff --git a/features/createroom/impl/src/main/res/values-ca/translations.xml b/features/createroom/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index f9e1dea0fc..0000000000 --- a/features/createroom/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - "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 ab72755dcf..66c4f08b70 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." + "Alle kan deltage i dette rum" "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 c3f42ca287..d1f5bfd283 100644 --- a/features/createroom/impl/src/main/res/values-de/translations.xml +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -8,19 +8,15 @@ "Neuer Chat" "Neuer Space" "Nur eingeladene Personen haben Zutritt zu diesem Chat." - "Privat" "Jeder kann diesen Chat finden. Du kannst dies jederzeit in den Einstellungen des Chats ändern." "Jeder kann beitreten." - "Öffentlich" "Jeder kann den Beitritt zum Chat erbitten, aber ein Admin oder Moderator muss die Anfrage akzeptieren." "Anfrage zum Beitritt zulassen" "Jeder in %1$s kann beitreten, aber alle anderen müssen den Beitritt anfragen." "Beitritt anfragen" "Nur eingeladene Personen können beitreten." - "Privat" "Jeder darf diesem Chat beitreten." - "Öffentlich" "Jeder in %1$s kann beitreten." "Standard" "Wer hat Zugang" @@ -28,8 +24,7 @@ Du kannst dies jederzeit in den Einstellungen des Chats ändern." "Adresse" " Sichtbarkeit des Chats" "(kein Space)" - "Nicht zu einem Space hinzufügen" - "Kein Space ausgewählt" + "Home" "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 821d55af67..27542ccc19 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-hr/translations.xml b/features/createroom/impl/src/main/res/values-hr/translations.xml index 17336bebf2..81979e3f84 100644 --- a/features/createroom/impl/src/main/res/values-hr/translations.xml +++ b/features/createroom/impl/src/main/res/values-hr/translations.xml @@ -3,34 +3,15 @@ "Nova soba" "Pozovi osobe" "Došlo je do pogreške prilikom stvaranja sobe" - "Prostor nije moguće stvoriti zbog nepoznate pogreške. Pokušajte ponovno kasnije." - "Dodaj ime…" "Nova soba" - "Novi prostor" - "Samo pozvane osobe mogu se pridružiti." - "Privatno" + "Samo pozvane osobe mogu pristupiti ovoj sobi. Sve su poruke sveobuhvatno šifrirane." "Svatko može pronaći ovu sobu. To možete u svakom trenutku promijeniti u postavkama sobe." - "Svatko se može pridružiti." - "Javno" "Svatko može zatražiti pridruživanje sobi, ali administrator ili moderator morat će prihvatiti zahtjev." - "Dopusti traženje pridruživanja" - "Svatko u %1$s može se pridružiti, ali svi ostali moraju zatražiti pristup." - "Zatraži pridruživanje" - "Samo pozvane osobe mogu pristupiti ovoj sobi. Sve su poruke sveobuhvatno šifrirane." - "Privatno" - "Svatko se može pridružiti." - "Javno" - "Svatko u %1$s može se pridružiti." - "Standard" - "Tko ima pristup" + "Zatraži pridruživanje" + "Svatko se može pridružiti ovoj sobi" "Da bi ova soba bila vidljiva u javnom direktoriju soba, trebat će vam adresa sobe." - "Adresa" + "Adresa sobe" "Vidljivost sobe" - "(bez razmaka)" - "Ne dodavaj u prostor" - "Nije odabran nijedan prostor" - "Dodaj u prostor" "Tema (neobavezno)" - "Dodaj opis…" 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 b602cb13d8..0164225ea2 100644 --- a/features/createroom/impl/src/main/res/values-pl/translations.xml +++ b/features/createroom/impl/src/main/res/values-pl/translations.xml @@ -3,34 +3,14 @@ "Nowy pokój" "Zaproś znajomych" "Wystąpił błąd w trakcie tworzenia pokoju" - "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" + "Tylko zaproszone osoby mogą dołączyć do tego pokoju. Wszystkie wiadomości są szyfrowane end-to-end." "Każdy może znaleźć ten pokój. Możesz to zmienić w ustawieniach 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" + "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" "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 33a5351877..a46fd1a1c4 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -3,34 +3,14 @@ "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" + "Doar persoanele invitate pot accesa această cameră. Toate mesajele sunt criptate end-to-end." "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" + "Cereți să vă alăturați" "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ă" + "Adresa camerei" "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 a40a2b21a5..d01da0dc35 100644 --- a/features/createroom/impl/src/main/res/values-uk/translations.xml +++ b/features/createroom/impl/src/main/res/values-uk/translations.xml @@ -3,32 +3,22 @@ "Нова кімната" "Запросити людей" "Під час створення кімнати сталася помилка" - "Простір не вдалося створити через невідому помилку. Спробуйте ще раз пізніше." "Додати назву…" "Нова кімната" "Новий простір" "Можуть приєднатися лише запрошені люди." - "Приватний" "Будь-хто може знайти цю кімнату. Ви можете змінити це в будь-який час у налаштуваннях кімнати." "Приєднатися може будь-хто." - "Публічний" "Будь-хто може подати запит на приєднання, але адміністратор або модератор повинен схвалити запит." "Дозволити запит на приєднання" - "Будь-хто з %1$s може приєднатися, але всі інші повинні подати запит на доступ." - "Запит на приєднання" "Приєднатися можуть лише запрошені особи." - "Приватний" "Приєднатися може будь-хто." - "Публічний" "Приєднатися може будь-хто з %1$s." - "Стандартний" "Хто має доступ" "Вам знадобиться адреса, щоб зробити її видимою в загальнодоступному каталозі." "Адреса" "Видимість кімнати" - "(без пробілу)" - "Не додавати до простору" "Головна" "Додати до простору" "Тема (необов\'язково)" diff --git a/features/createroom/impl/src/main/res/values-uz/translations.xml b/features/createroom/impl/src/main/res/values-uz/translations.xml index 88de696f64..98e246716d 100644 --- a/features/createroom/impl/src/main/res/values-uz/translations.xml +++ b/features/createroom/impl/src/main/res/values-uz/translations.xml @@ -3,34 +3,14 @@ "Yangi xona" "Odamlarni taklif qiling" "Xonani yaratishda xatolik yuz berdi" - "Noma’lum xatolik tufayli maydon yaratilmadi. Keyinroq qayta urining." - "Ism qo‘shish…" - "Yangi xona" - "Yangi maydon" "Faqat taklif etilgan shaxslargina bu xonaga kira oladi. Barcha xabarlar boshdan-oxirigacha shifrlanadi." - "Maxfiy" "Bu xonani har kim topishi mumkin. Buni xona sozlamalaridan istalgan vaqtda oʻzgartirishingiz mumkin." - "Istalgan kishi qo‘shilishi mumkin" - "Ommaviy" - "Istalgan kishi qo‘shilishni so‘rashi mumkin, lekin administrator yoki moderator so‘rovni qabul qilishi kerak." - "Qo‘shilish uchun ruxsat so‘rash" - "%1$s ichidagi har kim kirishi mumkin, lekin boshqalar ruxsat so‘rashi kerak." - "Qo‘shilish uchun so‘rash" - "Faqat taklif qilinganlar qo‘shilishi mumkin." - "Maxfiy" - "Istalgan kishi qo‘shilishi mumkin" - "Ommaviy" - "%1$s ichidagi har kim qo‘shilishi mumkin." - "Standart" - "Kimning kirish huquqi bor" - "Ommaviy katalogda ko‘rinadigan qilish uchun manzil kerak bo‘ladi." + "Xonaga qo‘shilishni istalgan kishi so‘rashi mumkin, lekin administrator yoki moderator so‘rovni qabul qilishi kerak" + "Qo‘shilishni so‘rang" + "Bu xonaga istalgan kishi qo‘shilishi mumkin" + "Ushbu xona ommaviy xonalar ro‘yxatida ko‘rinishi uchun sizga xona manzili kerak bo‘ladi." "Xona manzili" "Xonaning ko‘rinishi" - "(maydon yo‘q)" - "Maydonga kiritilmasin" - "Hech qanday maydon tanlanmagan" - "Maydonga qo‘shish" "Mavzu (ixtiyoriy)" - "Tavsif kiritish…" diff --git a/features/createroom/impl/src/main/res/values-vi/translations.xml b/features/createroom/impl/src/main/res/values-vi/translations.xml index cde672d7de..b10d15e077 100644 --- a/features/createroom/impl/src/main/res/values-vi/translations.xml +++ b/features/createroom/impl/src/main/res/values-vi/translations.xml @@ -4,12 +4,8 @@ "Mời ai đó" "Đã xảy ra lỗi khi tạo phòng." "Chỉ những người được mời mới có thể tham gia." - "Riêng tư" "Bất kỳ ai cũng có thể tìm thấy phòng này. Bạn có thể thay đổi cài đặt phòng bất cứ lúc nào." - "Công cộng" - "Riêng tư" - "Công cộng" "Chủ đề (tùy chọn)" "Thêm mô tả…" diff --git a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml index 0899f065d7..05495a0594 100644 --- a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml @@ -3,34 +3,14 @@ "建立聊天室" "邀請夥伴" "建立聊天室時發生錯誤" - "因為未知錯誤,無法建立空間。請稍後再試。" - "新增名稱……" - "新聊天室" - "新空間" - "僅被邀請的人才能加入。" - "私人" + "僅被邀請的人才能存取此聊天室。所有訊息均會端到端加密。" "任何人都可以找到此聊天室。 您隨時都可以在聊天室設定中變更此設定。" - "任何人都可以加入。" - "公開" - "任何人都可以要求加入,但管理員或版主必須接受該請求" - "允許要求加入" - "任何在 %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 1ba1036634..46d9654fbf 100644 --- a/features/createroom/impl/src/main/res/values-zh/translations.xml +++ b/features/createroom/impl/src/main/res/values-zh/translations.xml @@ -1,36 +1,36 @@ - "新房间" - "邀请人员" - "创建房间时出错" + "新聊天室" + "邀请朋友" + "创建聊天室时出错" "由于未知错误,空间创建失败。请稍后再试。" "添加名称…" - "新房间" + "新聊天室" "新空间" - "仅限受邀人员加入。" + "仅限受邀者加入。" "私密" - "任何人都能找到此房间。 -你可以随时在房间设置中更改。" - "任何人都可以加入" + "任何人都能找到此聊天室。 +你可以随时在聊天室设置中更改。" + "任何人都可以找到并加入" "公共" - "任何人都可申请加入,但需由管理员或协管员批准申请。" - "申请加入" - "%1$s 中的任何人都可以加入,但其他人必须申请访问。" + "任何人都可申请加入,但需由管理员或版主批准请求。" + "请求加入" + "%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 b9d7eb6e7d..fcedcb2367 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 @@ -301,9 +301,7 @@ class ConfigureRoomPresenterTest { roomName = 0, roomAvatar = 0, roomTopic = 0, - spaceChild = 0, - beacon = 0, - beaconInfo = 0, + spaceChild = 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..c0d625a45e 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 @@ -135,7 +135,7 @@ private fun ColumnScope.Buttons( ) { val logoutAction = state.accountDeactivationAction Button( - text = stringResource(CommonStrings.action_delete), + text = stringResource(CommonStrings.action_deactivate), showProgress = logoutAction is AsyncAction.Loading, destructive = true, enabled = state.submitEnabled, diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt index ab9d87c543..905112a78d 100644 --- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/AccountDeactivationConfirmationDialog.kt @@ -22,7 +22,7 @@ fun AccountDeactivationConfirmationDialog( ConfirmationDialog( title = stringResource(id = R.string.screen_deactivate_account_title), content = stringResource(R.string.screen_deactivate_account_confirmation_dialog_content), - submitText = stringResource(id = CommonStrings.action_delete), + submitText = stringResource(id = CommonStrings.action_deactivate), onSubmitClick = onSubmitClick, onDismiss = onDismiss, destructiveSubmit = true, diff --git a/features/deactivation/impl/src/main/res/values-bg/translations.xml b/features/deactivation/impl/src/main/res/values-bg/translations.xml index 936e4726a5..34ad5b4772 100644 --- a/features/deactivation/impl/src/main/res/values-bg/translations.xml +++ b/features/deactivation/impl/src/main/res/values-bg/translations.xml @@ -1,4 +1,5 @@ "Моля, потвърдете, че искате да деактивирате акаунта си. Това действие не може да бъде отменено." + "Деактивиране на акаунта" diff --git a/features/deactivation/impl/src/main/res/values-ca/translations.xml b/features/deactivation/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index c8ad39f7ae..0000000000 --- a/features/deactivation/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - "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 13659e1a75..e0f4fd14c7 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 smazat svůj účet. Tuto akci nelze vrátit zpět." + "Potvrďte prosím, že chcete svůj účet deaktivovat. Tuto akci nelze vrátit zpět." "Smazat všechny mé zprávy" "Upozornění: Budoucí uživatelé mohou vidět neúplné konverzace." - "Smazání účtu je %1$s, dojde k:" + "Deaktivace vašeho účtu je %1$s, což způsobí:" "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." - "Smazat účet" + "Deaktivovat úč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 f434547b8a..c6dcb1710a 100644 --- a/features/deactivation/impl/src/main/res/values-da/translations.xml +++ b/features/deactivation/impl/src/main/res/values-da/translations.xml @@ -1,14 +1,14 @@ - "Bekræft venligst, at du ønsker at slette din konto. Denne handling kan ikke fortrydes." + "Bekræft venligst, at du vil deaktivere din konto. Denne handling kan ikke fortrydes." "Slet alle mine beskeder" "Advarsel: Fremtidige brugere kan muligvis se ufuldstændige samtaler." - "Sletning af din konto er %1$s, det vil:" + "Deaktivering 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" + "Deaktiver 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 8430134d00..1aec7495a1 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 löschen möchtest. Diese Aktion kann nicht rückgängig gemacht werden." + "Bitte bestätige, dass du dein Konto deaktivieren möchtest. Dies kann nicht rückgängig gemacht werden." "Lösche alle meine Nachrichten" "Warnung: Künftigen Nutzern werden möglicherweise unvollständige Konversationen angezeigt." - "Das Löschen deines Kontos ist %1$s. Es wird:" + "Dein Konto zu deaktivieren ist %1$s. Folgendes wird passieren:" "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." - "Konto löschen" + "Nutzerkonto deaktivieren" 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 b6a359abbf..ac645f3063 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-es/translations.xml b/features/deactivation/impl/src/main/res/values-es/translations.xml index 17ae73d6c8..cd0757ba3e 100644 --- a/features/deactivation/impl/src/main/res/values-es/translations.xml +++ b/features/deactivation/impl/src/main/res/values-es/translations.xml @@ -10,4 +10,5 @@ "Te eliminará de todas las salas de chat." "Eliminará la información de tu cuenta de nuestro servidor de identidad." "Tus mensajes seguirán siendo visibles para los usuarios registrados, pero no estarán disponibles para los usuarios nuevos o no registrados si decides eliminarlos." + "Desactivar cuenta" 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 bfb41d7665..95695fef16 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 kustutada oma kasutajakonto. Seda tegevust ei saa tagasi pöörata." + "Palun kinnita uuesti, et soovid eemaldada oma konto kasutusest" "Kustuta kõik minu sõnumid" "Hoiatus: tulevased kasutajad võivad näha poolikuid vestlusi." - "Sinu konto kustutamine on %1$s ja sellega:" + "Sinu konto kasutusest eemaldamine 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." - "Kustuta kasutajakonto" + "Eemalda konto kasutusest" 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 148fe3d610..df2543be70 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 poistaa tilisi. Tätä ei voi perua." + "Vahvista, että haluat deaktivoida tilisi. Tätä ei voi perua." "Poista kaikki viestini" "Varoitus: Tulevaisuudessa muut voivat nähdä puutteellisia keskusteluja." - "Tilisi poistamista %1$s. Jos teet sen:" + "Tilisi deaktivointia %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." - "Poista tili" + "Deaktivoi 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 cf69cb3275..675ac1e1e0 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 supprimer votre compte. Cette action ne peut pas être annulée." + "Veuillez confirmer que vous souhaitez désactiver 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 suppression de votre compte est %1$s, cela va :" + "La désactivation 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." - "Supprimer le compte" + "Désactiver 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 1d8a02a08c..04148fdc48 100644 --- a/features/deactivation/impl/src/main/res/values-hr/translations.xml +++ b/features/deactivation/impl/src/main/res/values-hr/translations.xml @@ -10,5 +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" + "Deaktiviraj 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 2c3f51ed7a..3d3722b8ef 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 a fiókja törlését. Ez a művelet nem vonható vissza." + "Erősítse meg, hogy deaktiválja a fiókjá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." - "Fiókjának törlése: %1$s, ez a következőket eredményezi:" + "A fiók deaktiválása %1$s, a következőket okozza:" "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 törlése" + "Fiók deaktiválása" 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 e3de1ec8bb..3fbc9d536b 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 eliminare il tuo account. Questa azione è irreversibile." + "Conferma di voler disattivare il tuo account. Questa azione è irreversibile." "Elimina tutti i miei messaggi" "Attenzione: gli utenti futuri potrebbero vedere conversazioni incomplete." - "L\'eliminazione del tuo account è %1$s, e comporterà:" + "La disattivazione del tuo account è %1$s , quindi:" "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." - "Elimina account" + "Disattiva 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 53893a594f..873b1308ec 100644 --- a/features/deactivation/impl/src/main/res/values-ja/translations.xml +++ b/features/deactivation/impl/src/main/res/values-ja/translations.xml @@ -1,14 +1,14 @@ - "アカウントを削除しようとしていることを確認しています。この操作は元に戻せません。" + "アカウントを無効化することを再度確認します。この操作は元に戻せません。" "メッセージをすべて削除" "注意: 新しいユーザーには断片的な会話が表示されます" - "アカウントを削除することは %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..6b7953a4a5 100644 --- a/features/deactivation/impl/src/main/res/values-ko/translations.xml +++ b/features/deactivation/impl/src/main/res/values-ko/translations.xml @@ -10,4 +10,5 @@ "모든 채팅방에서 자신을 제거하세요." "당사의 신원 서버에서 귀하의 계정 정보를 삭제하세요." "메시지는 등록된 사용자에게는 계속 표시되지만, 삭제하면 신규 또는 미등록 사용자는 볼 수 없게 됩니다." + "계정 비활성화" 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 5778123aa5..bddb6a9037 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ź usunięcie konta. Tej akcji nie można cofnąć." + "Potwierdź dezaktywacje konta. Tej akcji nie można cofnąć." "Usuń wszystkie moje wiadomości" "Ostrzeżenie: Przyszli użytkownicy mogą zobaczyć niekompletne rozmowy." - "Usunięcie konta jest %1$s, co spowoduje:" + "Dezaktywacja konta jest %1$s, zostanie:" "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." - "Usuń konto" + "Dezaktywuj konto" diff --git a/features/deactivation/impl/src/main/res/values-pt-rBR/translations.xml b/features/deactivation/impl/src/main/res/values-pt-rBR/translations.xml index a986b18a7c..7000a65d47 100644 --- a/features/deactivation/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/deactivation/impl/src/main/res/values-pt-rBR/translations.xml @@ -10,4 +10,5 @@ "Te remover de todas as salas de conversa." "Apague as informações da sua conta do nosso servidor de identidade." "Suas mensagens ainda estarão visíveis para os usuários registrados, mas não estarão disponíveis para usuários novos ou não registrados se você optar por apagá-las." + "Desativar conta" 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 6176b4584e..acd4c0747d 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ă ștergeți contul. Această acțiune nu poate fi anulată." + "Vă rugăm să confirmați că doriți să vă dezactivaț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." - "Ștergerea contului dumneavoastră este %1$s, acesta va:" + "Dezactivarea 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." - "Ștergeți contul" + "Dezactivaț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 79c25af475..6f595cde29 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-uk/translations.xml b/features/deactivation/impl/src/main/res/values-uk/translations.xml index 62cf66cec1..04b32df8b2 100644 --- a/features/deactivation/impl/src/main/res/values-uk/translations.xml +++ b/features/deactivation/impl/src/main/res/values-uk/translations.xml @@ -10,5 +10,5 @@ "Видалити вас з усіх чатів." "Видаліть інформацію свого облікового запису з нашого сервера ідентифікації." "Ваші повідомлення залишатимуться видимими для зареєстрованих користувачів, але недоступними для нових або незареєстрованих користувачів, якщо ви вирішите їх видалити." - "Відключити обліковий запис" + "Деактивувати обліковий запис" diff --git a/features/deactivation/impl/src/main/res/values-ur/translations.xml b/features/deactivation/impl/src/main/res/values-ur/translations.xml index 3cad49aeb3..297b29c519 100644 --- a/features/deactivation/impl/src/main/res/values-ur/translations.xml +++ b/features/deactivation/impl/src/main/res/values-ur/translations.xml @@ -10,4 +10,5 @@ "آپ کو تمام چیت رومز سے ہٹا دے گا۔" "ہمارے شناختی سرور سے اپنے اکاؤنٹ کی معلومات کو حذف کریں۔" "آپ کے پیغامات اب بھی رجسٹرڈ صارفین کو نظر آئیں گے لیکن اگر آپ انہیں حذف کرنے کا انتخاب کرتے ہیں تو نئے یا غیر رجسٹرڈ صارفین کے لیے دستیاب نہیں ہوں گے۔" + "اکاؤنٹ کو غیر فعال کریں" 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 e0dcfe59ef..19a70bb149 100644 --- a/features/deactivation/impl/src/main/res/values-uz/translations.xml +++ b/features/deactivation/impl/src/main/res/values-uz/translations.xml @@ -10,5 +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" + "Hisobni faolsizlantirish" diff --git a/features/deactivation/impl/src/main/res/values-vi/translations.xml b/features/deactivation/impl/src/main/res/values-vi/translations.xml index f3b48163ed..22bc0a6d6e 100644 --- a/features/deactivation/impl/src/main/res/values-vi/translations.xml +++ b/features/deactivation/impl/src/main/res/values-vi/translations.xml @@ -1,13 +1,7 @@ - "Vui lòng xác nhận rằng bạn muốn vô hiệu hóa tài khoản của mình. Hành động này không thể hoàn tác." "Xóa tất cả tin nhắn của tôi" "Cảnh báo: Người dùng sau này có thể thấy các cuộc trò chuyện chưa hoàn chỉnh." - "Việc vô hiệu hóa tài khoản của bạn là %1$s , nó sẽ:" - "không thể đảo ngược" - "%1$s Tài khoản của bạn (bạn không thể đăng nhập lại và ID của bạn không thể được sử dụng lại)." - "Vô hiệu hóa vĩnh viễn" - "Loại bỏ bạn khỏi tất cả các phòng chat." - "Xóa thông tin tài khoản của bạn khỏi máy chủ nhận dạng của chúng tôi." "Tin nhắn của bạn vẫn sẽ hiển thị cho người dùng đã đăng ký nhưng sẽ không hiển thị cho người dùng mới hoặc chưa đăng ký nếu bạn chọn xóa chúng." + "Vô hiệu hóa tài khoản" diff --git a/features/deactivation/impl/src/main/res/values-zh/translations.xml b/features/deactivation/impl/src/main/res/values-zh/translations.xml index 3916921652..ca24375d66 100644 --- a/features/deactivation/impl/src/main/res/values-zh/translations.xml +++ b/features/deactivation/impl/src/main/res/values-zh/translations.xml @@ -1,14 +1,14 @@ - "请确认要删除的账户。此操作无法撤消。" + "请确认您要停用您的账户。此操作无法撤消。" "删除我的所有消息" "警告:未来的用户可能会看到不完整的对话。" - "正在删除的账户为 %1$s,它将:" - "不可逆" - "你的账户 %1$s(将无法再登录,并且 ID 无法重复使用)。" + "停用您的帐户是%1$s,它将:" + "不可逆转的" + "%1$s您的账户(您无法登录回来,并且您的ID无法重复使用)。" "永久禁用" - "将你从所有聊天房间中移除。" - "从我们的身份服务器中删除你的账户信息。" - "注册用户仍可看到你的消息,但如果选择删除它们,新用户或未注册用户将无法看到你的消息。" - "删除账户" + "将您从所有聊天房间中移除。" + "从我们的身份服务器中删除您的账户信息。" + "注册用户仍可看到您的消息,但如果您选择删除它们,新用户或未注册用户将无法看到您的消息。" + "停用账户" diff --git a/features/deactivation/impl/src/main/res/values/localazy.xml b/features/deactivation/impl/src/main/res/values/localazy.xml index fc12c7d2f8..0380cf1c94 100644 --- a/features/deactivation/impl/src/main/res/values/localazy.xml +++ b/features/deactivation/impl/src/main/res/values/localazy.xml @@ -1,14 +1,14 @@ - "Please confirm that you want to delete your account. This action cannot be undone." + "Please confirm that you want to deactivate your account. This action cannot be undone." "Delete all my messages" "Warning: Future users may see incomplete conversations." - "Deleting your account is %1$s, it will:" + "Deactivating your account is %1$s, it will:" "irreversible" "%1$s your account (you can\'t log back in, and your ID can\'t be reused)." "Permanently disable" "Remove you from all chat rooms." "Delete your account information from our identity server." "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them." - "Delete account" + "Deactivate account" 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 c672fd666b..eff479d21c 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.deactivation.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -29,29 +26,33 @@ 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 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() + @Test - fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setAccountDeactivationView( + rule.setAccountDeactivationView( state = anAccountDeactivationState(eventSink = eventsRecorder), onBackClick = it, ) - pressBack() + rule.pressBack() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Deactivate emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on Deactivate emits the expected Event`() { val eventsRecorder = EventsRecorder() - setAccountDeactivationView( + rule.setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -59,14 +60,14 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_delete) + rule.clickOn(CommonStrings.action_deactivate) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false)) } @Test - fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() { val eventsRecorder = EventsRecorder() - setAccountDeactivationView( + rule.setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -75,14 +76,14 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - pressTag(TestTags.dialogPositive.value) + rule.pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false)) } @Test - fun `clicking on retry on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on retry on the confirmation dialog emits the expected Event`() { val eventsRecorder = EventsRecorder() - setAccountDeactivationView( + rule.setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -91,26 +92,26 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_retry) + rule.clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true)) } @Test - fun `switching on the erase all switch emits the expected Event`() = runAndroidComposeUiTest { + fun `switching on the erase all switch emits the expected Event`() { val eventsRecorder = EventsRecorder() - setAccountDeactivationView( + rule.setAccountDeactivationView( state = anAccountDeactivationState( eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_deactivate_account_delete_all_messages) + rule.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`() = runAndroidComposeUiTest { + fun `switching off the erase all switch emits the expected Event`() { val eventsRecorder = EventsRecorder() - setAccountDeactivationView( + rule.setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( eraseData = true, @@ -118,15 +119,15 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_deactivate_account_delete_all_messages) + rule.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`() = runAndroidComposeUiTest { + fun `typing text in the password field emits the expected Event`() { val eventsRecorder = EventsRecorder() - setAccountDeactivationView( + rule.setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -134,12 +135,12 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - onNodeWithTag(TestTags.loginPassword.value).performTextInput("A") + rule.onNodeWithTag(TestTags.loginPassword.value).performTextInput("A") eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD")) } } -private fun AndroidComposeUiTest.setAccountDeactivationView( +private fun AndroidComposeTestRule.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 92d8b9b646..65fe3fe087 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,7 +16,6 @@ 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 f87dc743e8..6bd6c78de5 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,7 +10,6 @@ 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 6e3ed5d3cc..932d082fd9 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 9aafcd343c..3441063a8a 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,6 +15,5 @@ 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 c37fc53de3..542e73717a 100644 --- a/features/enterprise/test/build.gradle.kts +++ b/features/enterprise/test/build.gradle.kts @@ -15,7 +15,6 @@ 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 805c75be6a..3c17a4de7c 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,7 +30,6 @@ 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) @@ -39,10 +38,6 @@ 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 0bcad13033..3914c60155 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,15 +14,10 @@ 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/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 57a9f65099..f1e9bd8fc6 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId @@ -24,30 +21,34 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ForwardMessagesViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `cancel error emits the expected event`() = runAndroidComposeUiTest { + fun `cancel error emits the expected event`() { val eventsRecorder = EventsRecorder() - setForwardMessagesView( + rule.setForwardMessagesView( aForwardMessagesState( forwardAction = AsyncAction.Failure(AN_EXCEPTION), eventSink = eventsRecorder ), ) - pressTag(TestTags.dialogPositive.value) + rule.pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(ForwardMessagesEvents.ClearError) } @Test - fun `success invokes onForwardSuccess`() = runAndroidComposeUiTest { + fun `success invokes onForwardSuccess`() { val data = listOf(A_ROOM_ID) val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam?>(data) { callback -> - setForwardMessagesView( + rule.setForwardMessagesView( aForwardMessagesState( forwardAction = AsyncAction.Success(data), eventSink = eventsRecorder @@ -58,7 +59,7 @@ class ForwardMessagesViewTest { } } -private fun AndroidComposeUiTest.setForwardMessagesView( +private fun AndroidComposeTestRule.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 1bfa10daf2..0ca25c9455 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,9 +19,6 @@ 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 @@ -93,11 +90,7 @@ fun ChooseSelfVerificationModeView( Text( modifier = Modifier .clickable(onClick = onLearnMore) - .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 - }, + .padding(vertical = 4.dp, horizontal = 16.dp), 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 deleted file mode 100644 index 1b645dd5eb..0000000000 --- a/features/ftue/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - "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 0e83b29e35..474d085df0 100644 --- a/features/ftue/impl/src/main/res/values-de/translations.xml +++ b/features/ftue/impl/src/main/res/values-de/translations.xml @@ -2,8 +2,8 @@ "Bestätigung unmöglich?" "Erstelle einen neuen Wiederherstellungsschlüssel" - "Wähle eine Verifizierungsmethode, um den sicheren Nachrichtenversand einzurichten." - "Bestätige deine digitale Identität" + "Verifiziere dieses Gerät, um sichere Chats einzurichten." + "Bestätige deine 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 c013556568..4790fdb716 100644 --- a/features/ftue/impl/src/main/res/values-et/translations.xml +++ b/features/ftue/impl/src/main/res/values-et/translations.xml @@ -2,8 +2,8 @@ "Kas kinnitamine pole võimalik?" "Loo uus taastevõti" - "Turvalise sõnumside seadistamiseks vali verifitseerimise viis." - "Kinnita oma digitaalne identiteet" + "Krüptitud sõnumivahetuse tagamiseks verifitseeri see seade." + "Kinnita, et see oled sina" "Kasuta teist seadet" "Kasuta taastevõtit" "Nüüd saad saata või lugeda sõnumeid turvaliselt ning kõik sinu vestluspartnerid võivad usaldada seda seadet." diff --git a/features/ftue/impl/src/main/res/values-hr/translations.xml b/features/ftue/impl/src/main/res/values-hr/translations.xml index f5aeb642b1..d535c660a2 100644 --- a/features/ftue/impl/src/main/res/values-hr/translations.xml +++ b/features/ftue/impl/src/main/res/values-hr/translations.xml @@ -2,8 +2,8 @@ "Ne možete potvrditi?" "Izradi novi ključ za oporavak" - "Odaberite način potvrde za postavljanje sigurne razmjene poruka." - "Potvrdite svoj digitalni identitet" + "Potvrdite ovaj uređaj kako biste postavili sigurnu razmjenu poruka." + "Potvrdite svoj identitet" "Upotrijebite drugi uređaj" "Upotrijebi ključ za oporavak" "Sada možete sigurno čitati ili slati poruke, a svatko s kim razgovarate također može vjerovati ovom uređaju." 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 9a87a3dcfa..68b69079ef 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 45ca82ede6..5d77c57994 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" - "Wybierz sposób weryfikacji, aby skonfigurować bezpieczne wiadomości." - "Potwierdź swoją tożsamość cyfrową" + "Zweryfikuj to urządzenie, aby skonfigurować bezpieczne przesyłanie wiadomości." + "Potwierdź, że to Ty" "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 34f39c7bdb..5b6729f04e 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 a tua identidade digital" + "Confirma que és tu" "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-ro/translations.xml b/features/ftue/impl/src/main/res/values-ro/translations.xml index 85b151faa8..abf72140e8 100644 --- a/features/ftue/impl/src/main/res/values-ro/translations.xml +++ b/features/ftue/impl/src/main/res/values-ro/translations.xml @@ -2,8 +2,8 @@ "Nu puteți confirma?" "Creați o nouă cheie de recuperare" - "Alegeți cum doriți să vă verificați pentru a configura mesageria securizată." - "Confirmați-vă identitatea digitală" + "Verificați acest dispozitiv pentru a configura mesagerie securizată." + "Confirmați că sunteți dumneavoastră" "Utilizați un alt dispozitiv" "Utilizați cheia de recuperare" "Acum puteți citi sau trimite mesaje în siguranță, iar oricine cu care conversați poate avea încredere în acest dispozitiv." diff --git a/features/ftue/impl/src/main/res/values-uz/translations.xml b/features/ftue/impl/src/main/res/values-uz/translations.xml index 2279bb6c92..8edff2c305 100644 --- a/features/ftue/impl/src/main/res/values-uz/translations.xml +++ b/features/ftue/impl/src/main/res/values-uz/translations.xml @@ -2,8 +2,8 @@ "Tasdiqlay olmayapsizmi?" "Yangi tiklash kalitini yarating" - "Xavfsiz xabar almashinuvni sozlash uchun tasdiqlash usulini tanlang." - "Raqamli shaxsingizni tasdiqlang" + "Xavfsiz xabarlashuvni sozlash uchun ushbu qurilmani tasdiqlang." + "Shaxsingizni tasdiqlang" "Boshqa qurilmadan foydalanish" "Qayta tiklash kalitidan foydalaning" "Endi xabarlarni xavfsiz tarzda o‘qish yoki yuborish imkoniyatiga egasiz, shuningdek, siz bilan muloqot qilayotgan har qanday kishi ham bu qurilmaga ishonch bildirishi mumkin." diff --git a/features/ftue/impl/src/main/res/values-vi/translations.xml b/features/ftue/impl/src/main/res/values-vi/translations.xml index 6ea7d8bf91..c70d9be0fb 100644 --- a/features/ftue/impl/src/main/res/values-vi/translations.xml +++ b/features/ftue/impl/src/main/res/values-vi/translations.xml @@ -1,14 +1,9 @@ - "Không thể xác nhận?" - "Tạo khóa khôi phục mới" "Chọn phương thức xác minh để bật nhắn tin bảo mật." "Xác nhận danh tính kỹ thuật số của bạn" - "Dùng thiết bị khác" - "Sử dụng khóa khôi phục" "Giờ đây bạn có thể đọc và gửi tin nhắn một cách an toàn, và những người bạn trò chuyện cũng có thể tin tưởng thiết bị này." "Thiết bị được xác thực" - "Dùng thiết bị khác" "Đang chờ trên thiết bị khác…" "Bạn có thể thay đổi cài đặt sau." "Cho phép thông báo để không bỏ lỡ bất kỳ tin nhắn nào" diff --git a/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml b/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml index 9bad2d1705..6340efdbc3 100644 --- a/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml @@ -2,8 +2,8 @@ "無法確認?" "建立新的復原金鑰" - "選擇驗證方式以設定安全訊息傳遞。" - "確認您的數位身份" + "驗證這部裝置以設定安全通訊。" + "確認這是你本人" "使用另一部裝置" "使用復原金鑰" "您可以安全地讀取和發送訊息了,與您聊天的人也可以信任這部裝置。" 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 f9171c9879..68a48831e0 100644 --- a/features/ftue/impl/src/main/res/values-zh/translations.xml +++ b/features/ftue/impl/src/main/res/values-zh/translations.xml @@ -3,13 +3,13 @@ "无法确认?" "创建新的恢复密钥" "选择验证方式以设置安全的消息传输。" - "确认你的数字身份" - "使用其它设备" + "确认您的数字身份" + "使用其他设备" "使用恢复密钥" - "现在你可以安全地读取或发送消息,并且与你聊天的任何人也可以信任此设备。" + "现在,您可以安全地阅读或发送消息,与您聊天的人也会信任此设备。" "设备已验证" - "使用其它设备" - "正在等待其它设备…" - "你可以稍后更改设置。" + "使用其他设备" + "正在等待其他设备……" + "您可以稍后更改设置。" "允许通知,绝不错过任何消息" 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 6e74f58f66..521bf91b37 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.ftue.impl.R import io.element.android.libraries.architecture.AsyncData @@ -21,61 +18,65 @@ 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 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() + @Config(qualifiers = "h1024dp") @Test - fun `clicking on learn more invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on learn more invokes the expected callback`() { ensureCalledOnce { callback -> - setChooseSelfVerificationModeView( + rule.setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(), onLearnMoreClick = callback, ) - clickOn(CommonStrings.action_learn_more) + rule.clickOn(CommonStrings.action_learn_more) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on use another device calls the callback`() = runAndroidComposeUiTest { + fun `clicking on use another device calls the callback`() { ensureCalledOnce { callback -> - setChooseSelfVerificationModeView( + rule.setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseAnotherDevice = true))), onUseAnotherDevice = callback, ) - clickOn(R.string.screen_identity_use_another_device) + rule.clickOn(R.string.screen_identity_use_another_device) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on enter recovery key calls the callback`() = runAndroidComposeUiTest { + fun `clicking on enter recovery key calls the callback`() { ensureCalledOnce { callback -> - setChooseSelfVerificationModeView( + rule.setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseRecoveryKey = true))), onEnterRecoveryKey = callback, ) - clickOn(R.string.screen_identity_confirmation_use_recovery_key) + rule.clickOn(R.string.screen_identity_confirmation_use_recovery_key) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on cannot confirm calls the reset keys callback`() = runAndroidComposeUiTest { + fun `clicking on cannot confirm calls the reset keys callback`() { ensureCalledOnce { callback -> - setChooseSelfVerificationModeView( + rule.setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(), onResetKey = callback, ) - clickOn(R.string.screen_identity_confirmation_cannot_confirm) + rule.clickOn(R.string.screen_identity_confirmation_cannot_confirm) } } - private fun AndroidComposeUiTest.setChooseSelfVerificationModeView( + private fun AndroidComposeTestRule.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 0635da39a5..b36ee6aed2 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -46,7 +46,6 @@ 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 00f285e3f4..81e7969080 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,7 +17,6 @@ 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 @@ -172,7 +171,6 @@ 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 5f55beaf60..ff0fc00496 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 @@ -57,7 +57,6 @@ 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 @@ -66,8 +65,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 @@ -238,7 +237,6 @@ private fun SpaceFilterButton( else -> Unit } } - val isSelected = spaceFiltersState is SpaceFiltersState.Selected IconButton( onClick = ::onClick, @@ -322,15 +320,7 @@ private fun AccountIcon( Avatar( avatarData = avatarData, avatarType = AvatarType.User, - contentDescription = if (isCurrentAccount) { - if (showAvatarIndicator) { - stringResource(CommonStrings.a11y_settings_with_required_action) - } else { - stringResource(CommonStrings.common_settings) - } - } else { - null - }, + contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null, ) if (showAvatarIndicator) { RedIndicatorAtom( @@ -347,7 +337,7 @@ private fun AccountIcon( internal fun HomeTopBarPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - currentUserAndNeighbors = persistentListOf(aMatrixUser(id = "@id:domain", displayName = USER_NAME_ALICE)), + currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = false, areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), @@ -368,7 +358,7 @@ internal fun HomeTopBarPreview() = ElementPreview { internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - currentUserAndNeighbors = persistentListOf(aMatrixUser(id = "@id:domain", displayName = USER_NAME_ALICE)), + currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = false, areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), @@ -389,7 +379,7 @@ internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview { internal fun HomeTopBarSpacesPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Spaces, - currentUserAndNeighbors = persistentListOf(aMatrixUser(id = "@id:domain", displayName = USER_NAME_ALICE)), + currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = false, areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), @@ -410,7 +400,7 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview { internal fun HomeTopBarWithIndicatorPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - currentUserAndNeighbors = persistentListOf(aMatrixUser(id = "@id:domain", displayName = USER_NAME_ALICE)), + currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "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/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index 72ecb5046d..f541417104 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -29,7 +29,6 @@ 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 @@ -223,17 +222,20 @@ private fun NameAndTimestampRow( modifier = modifier.fillMaxWidth(), horizontalArrangement = spacedBy(16.dp) ) { - 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 - ) + 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 + ) + } // Timestamp Text( text = timestamp ?: "", @@ -260,12 +262,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, ) } } @@ -324,9 +326,7 @@ private fun MessagePreviewAndIndicatorRow( val messagePreview = room.latestEvent.content() val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.orEmpty().toString()) Text( - modifier = Modifier - .weight(1f) - .clipToBounds(), + modifier = Modifier.weight(1f), text = annotatedMessagePreview, color = ElementTheme.colors.roomListRoomMessage, style = ElementTheme.typography.fontBodyMdRegular, @@ -381,9 +381,7 @@ private fun InviteNameAndIndicatorRow( verticalAlignment = Alignment.CenterVertically, ) { Text( - modifier = Modifier - .weight(1f) - .clipToBounds(), + modifier = Modifier.weight(1f), 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 56b2c1ade4..3ff4339bb2 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): Job { - return roomList + fun launchIn(coroutineScope: CoroutineScope) { + roomList .summaries .onEach { roomSummaries -> replaceWith(roomSummaries) @@ -212,7 +212,6 @@ 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 e34f2845da..26054d7e56 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt @@ -19,6 +19,7 @@ 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 09e6c2e6c9..eefb2d6484 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt @@ -11,10 +11,6 @@ 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 @@ -89,16 +85,16 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider Unit, ) { Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) + modifier = Modifier.fillMaxWidth() ) { ListItem( headlineContent = { @@ -217,16 +212,23 @@ private fun RoomListModalBottomSheetContent( } } +// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up. +// see: https://issuetracker.google.com/issues/283843380 +// Remove this preview when the issue is fixed. @PreviewsDayNight @Composable -internal fun RoomListContextMenuPreview( +internal fun RoomListModalBottomSheetContentPreview( @PreviewParameter(RoomListStateContextMenuShownProvider::class) contextMenu: RoomListState.ContextMenu.Shown ) = ElementPreview { - RoomListContextMenu( + RoomListModalBottomSheetContent( contextMenu = contextMenu, canReportRoom = true, + onRoomMarkReadClick = {}, + onRoomMarkUnreadClick = {}, onRoomSettingsClick = {}, + onLeaveRoomClick = {}, + onFavoriteChange = {}, + onClearCacheRoomClick = {}, onReportRoomClick = {}, - eventSink = {}, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt index 0a7a29ebc2..523e677a57 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt @@ -13,21 +13,16 @@ 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.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.appconfig.ProtectionConfig import io.element.android.compound.theme.ElementTheme import io.element.android.features.home.impl.R import io.element.android.features.home.impl.model.RoomListRoomSummary -import io.element.android.libraries.core.extensions.toSafeLength 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 @@ -47,14 +42,9 @@ fun RoomListDeclineInviteMenu( ) { ModalBottomSheet( onDismissRequest = { eventSink(RoomListEvent.HideDeclineInviteMenu) }, - scrollable = false, ) { RoomListDeclineInviteMenuContent( - roomName = menu.roomSummary.name?.toSafeLength( - maxLength = ProtectionConfig.MAX_ROOM_NAME_LENGTH, - ellipsize = true, - ) - ?: menu.roomSummary.roomId.value, + roomName = menu.roomSummary.name ?: menu.roomSummary.roomId.value, onDeclineClick = { eventSink(RoomListEvent.HideDeclineInviteMenu) eventSink(RoomListEvent.DeclineInvite(menu.roomSummary, false)) @@ -84,8 +74,7 @@ private fun RoomListDeclineInviteMenuContent( Column( modifier = Modifier .fillMaxWidth() - .padding(all = 16.dp) - .verticalScroll(rememberScrollState()), + .padding(all = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( @@ -123,15 +112,16 @@ private fun RoomListDeclineInviteMenuContent( } } +// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up. +// see: https://issuetracker.google.com/issues/283843380 +// Remove this preview when the issue is fixed. @PreviewsDayNight @Composable -internal fun RoomListDeclineInviteMenuPreview( - @PreviewParameter(RoomListStateDeclineInviteMenuShownProvider::class) menu: RoomListState.DeclineInviteMenu.Shown, -) = ElementPreview { - RoomListDeclineInviteMenu( - menu = menu, - canReportRoom = false, +internal fun RoomListDeclineInviteMenuContentPreview() = ElementPreview { + RoomListDeclineInviteMenuContent( + roomName = "Room name", + onCancelClick = {}, + onDeclineClick = {}, onDeclineAndBlockClick = {}, - eventSink = {}, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateDeclineInviteMenuShownProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateDeclineInviteMenuShownProvider.kt deleted file mode 100644 index 73d4785e96..0000000000 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateDeclineInviteMenuShownProvider.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.home.impl.roomlist - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.tooling.preview.datasource.LoremIpsum -import io.element.android.features.home.impl.model.RoomListRoomSummary -import io.element.android.features.home.impl.model.aRoomListRoomSummary - -open class RoomListStateDeclineInviteMenuShownProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aDeclineInviteMenuShown(), - aDeclineInviteMenuShown( - aRoomListRoomSummary( - name = LoremIpsum(500).values.first(), - ) - ), - aDeclineInviteMenuShown( - aRoomListRoomSummary( - name = null, - ) - ), - ) -} - -internal fun aDeclineInviteMenuShown( - roomSummary: RoomListRoomSummary = aRoomListRoomSummary(), -) = RoomListState.DeclineInviteMenu.Shown( - roomSummary = roomSummary, -) 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..fb77c74203 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 @@ -81,8 +81,7 @@ fun SpaceFiltersView( if (state is SpaceFiltersState.Selecting) { state.eventSink(SpaceFiltersEvent.Selecting.Cancel) } - }, - scrollable = false, + } ) { Box( modifier = Modifier diff --git a/features/home/impl/src/main/res/values-ca/translations.xml b/features/home/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index e37db27f23..0000000000 --- a/features/home/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - "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-de/translations.xml b/features/home/impl/src/main/res/values-de/translations.xml index ee6cef29de..f504d5c8e1 100644 --- a/features/home/impl/src/main/res/values-de/translations.xml +++ b/features/home/impl/src/main/res/values-de/translations.xml @@ -5,9 +5,9 @@ "Kommen die Benachrichtigungen nicht an?" "Dein Benachrichtigungs-Ping wurde aktualisiert – klarer, schneller und weniger störend." "Wir haben deine Sounds aktualisiert" - "Deine Chats werden automatisch gesichert und mit einer Ende-zu-Ende-Verschlüsselung geschützt. Um dieses Backup wiederherzustellen und deine digitale Identität zu bewahren, falls du den Zugriff auf alle deine Geräte verlierst, benötigst du deinen Wiederherstellungsschlüssel." - "Wiederherstellungsschlüssel einrichten" - "Sichere deine Chats" + "Stelle Deine kryptographische Identität und Deinen Nachrichtenverlauf mit Hilfe eines Wiederherstellungsschlüssels wieder her, falls du alle deine Geräte verloren haben solltest" + "Wiederherstellung einrichten" + "Wiederherstellung einrichten" "Bestätige deinen Wiederherstellungsschlüssel, um weiterhin auf deinen Schlüsselspeicher und den Nachrichtenverlauf zugreifen zu können." "Gib deinen Wiederherstellungsschlüssel ein" "Hast du deinen Wiederherstellungsschlüssel vergessen?" 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 4ba1695d7e..c1f61bfa29 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" - "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" + "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" "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 b0c6a30bc4..aec50309d8 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,7 +25,6 @@ "آغاز با پیام دادن به کسی." "هنوز گپی وجود ندارد." "علاقه‌مندی‌ها" - "می‌توانید در تنظیمات چت، یک چت را به موارد دلخواه خود اضافه کنید. فعلاً می‌توانید فیلترها را غیرفعال کنید تا چت‌های دیگر خود را ببینید." "هنوز هیچ گپ مورد علاقه‌ای ندارید" "دعوت‌ها" "هیچ دعوت منتظری ندارید." diff --git a/features/home/impl/src/main/res/values-hr/translations.xml b/features/home/impl/src/main/res/values-hr/translations.xml index eae429a0e5..233cac78d8 100644 --- a/features/home/impl/src/main/res/values-hr/translations.xml +++ b/features/home/impl/src/main/res/values-hr/translations.xml @@ -5,9 +5,9 @@ "Obavijesti ne stižu?" "Vaš je signal obavijesti ažuriran – jasniji je, brži i manje ometajući." "Ažurirali smo vaše zvukove" - "Vaši se razgovori automatski sigurnosno kopiraju enkripcijom od početka do kraja. Da biste vratili ovu sigurnosnu kopiju i zadržali svoj digitalni identitet kada izgubite pristup svim svojim uređajima, trebat će vam ključ za oporavak." - "ključ za oporavak" - "Napravite sigurnosnu kopiju svojih razgovora" + "Ako ste izgubili sve postojeće uređaje, oporavite svoj kriptografski identitet i povijest poruka pomoću ključa za oporavak." + "Postavljanje oporavka" + "Postavite oporavak kako biste zaštitili svoj račun" "Potvrdite svoj ključ za oporavak kako biste zadržali pristup pohrani ključeva i povijesti poruka." "Unesite svoj ključ za oporavak" "Zaboravili ste ključ za oporavak?" @@ -50,7 +50,6 @@ Nemate nepročitanih poruka!" "Označi kao pročitano" "Označi kao nepročitano" "Ova je soba nadograđena" - "Vaši prostori" "Izgleda da koristite novi uređaj. Izvršite provjeru drugim uređajem da biste pristupili svojim šifriranim porukama." "Potvrdi identitet" 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..3e4b16b682 100644 --- a/features/home/impl/src/main/res/values-ja/translations.xml +++ b/features/home/impl/src/main/res/values-ja/translations.xml @@ -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 0c1b07f27b..73e0b5e52e 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" - "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" + "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" "Potwierdź klucz przywracania, aby zachować dostęp do magazynu kluczy i historii wiadomości." "Wprowadź klucz przywracania" "Zapomniałeś klucza przywracania?" @@ -50,7 +50,6 @@ 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 6c6a72930e..9870014904 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." - "Chave de recuperação" + "Configurar 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 14cf100c22..e4a80b4fb7 100644 --- a/features/home/impl/src/main/res/values-ro/translations.xml +++ b/features/home/impl/src/main/res/values-ro/translations.xml @@ -5,9 +5,9 @@ "Nu primiți notificări?" "Sunetul pentru notificări a fost actualizat — mai clar, mai rapid și mai puțin perturbatoar." "Am reîmprospătat sunetele" - "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 pierzdeți accesul la toate dispozitivele dumneavoastră, veți avea nevoie de cheia de recuperare." - "Obțineți cheia de recuperare" - "Faceți un backup al mesajelor" + "Recuperați-vă identitatea criptografică și mesajele anterioare cu o cheie de recuperare dacă ați pierdut toate dispozitivele existente." + "Configurați recuperarea" + "Configurați recuperarea pentru a vă proteja contul" "Backup-ul pentru chat nu este sincronizat. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup." "Introduceți cheia de recuperare" "Ați uitat cheia de recuperare?" @@ -50,7 +50,6 @@ 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-uz/translations.xml b/features/home/impl/src/main/res/values-uz/translations.xml index 6079389609..e5ef24e80a 100644 --- a/features/home/impl/src/main/res/values-uz/translations.xml +++ b/features/home/impl/src/main/res/values-uz/translations.xml @@ -5,9 +5,9 @@ "Bildirishnoma kelmayaptimi?" "Xabarnoma signali yangilandi — endi u aniqroq, tezroq va kamroq halal beradigan bo‘ldi." "Tovushlaringiz yangilandi" - "Chatlaringiz avtomatik ravishda boshidan oxirigacha shifrlash bilan zaxiralanadi. Bu zaxirani tiklash va barcha qurilmalaringizdan foydalana olmay qolganingizda raqamli identifikatoringizni saqlab qolish uchun sizga tiklash kaliti kerak bo‘ladi." + "Mavjud barcha qurilmalarni yoʻqotgan boʻlsangiz, kriptografik kimligingizni va xabarlar tarixini qayta tiklovchi kalit bilan saqlab qoʻying." "Qayta tiklashni sozlang" - "Chatlaringizni zaxiralang" + "Hisobingizni himoya qilish uchun tiklashni sozlang" "Kalit saqlash joyingiz va xabarlar tarixingizga kirishni saqlab qolish uchun tiklash kalitingizni tasdiqlang." "Qayta tiklash kalitingizni kiriting" "Tiklash kalitini unutdingizmi?" @@ -50,7 +50,6 @@ Sizda oʻqilmagan xabarlar yoʻq!" "Oʻqilgan deb belgilash" "Oʻqilmagan deb belgilash" "Bu xona yangilandi" - "Maydonlaringiz" "Siz yangi qurilmadan foydalanayotganga o‘xshaysiz. Shifrlangan xabarlaringizga kirish uchun boshqa qurilma bilan tasdiqlang." "Siz ekanligingizni tasdiqlang" diff --git a/features/home/impl/src/main/res/values-vi/translations.xml b/features/home/impl/src/main/res/values-vi/translations.xml index 49fdaf46c9..bc62880fff 100644 --- a/features/home/impl/src/main/res/values-vi/translations.xml +++ b/features/home/impl/src/main/res/values-vi/translations.xml @@ -12,8 +12,6 @@ "Nhập khóa khôi phục của bạn." "Bạn quên khóa khôi phục?”" "Dữ liệu khóa của bạn không còn đồng bộ" - "Để đảm bảo bạn không bỏ lỡ bất kỳ cuộc gọi quan trọng nào, vui lòng thay đổi cài đặt để cho phép thông báo toàn màn hình khi điện thoại của bạn bị khóa." - "Nâng cao trải nghiệm cuộc gọi của bạn" "Cuộc trò chuyện" "Bạn có chắc muốn từ chối lời mời tham gia %1$s không?" "Từ chối lời mời" diff --git a/features/home/impl/src/main/res/values-zh-rTW/translations.xml b/features/home/impl/src/main/res/values-zh-rTW/translations.xml index b52ac9ad8b..20ee59da98 100644 --- a/features/home/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/home/impl/src/main/res/values-zh-rTW/translations.xml @@ -5,9 +5,9 @@ "沒收到通知?" "您的通知提示音已更新,更清晰、更快、更不易分心。" "我們已更新您的音效設定" - "您的聊天會自動使用端到端加密備份。若您失去對您所有裝置的存取權,且要還原此備份並保留您的數位身份的話,您就會需要您的還原金鑰。" - "取得還原金鑰" - "備份您的聊天" + "若您遺失了所有現有裝置,則請使用復原金鑰以救援您的密碼學身份與訊息歷史紀錄。" + "設定復原" + "設定備援以保護您的帳號" "確認您的復原金鑰以維持對金鑰儲存空間與訊息歷史紀錄的存取權。" "輸入您的復原金鑰" "忘記了您的復原金鑰?" @@ -50,7 +50,6 @@ "標為已讀" "標為未讀" "此聊天室已升級" - "您的空間" "您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。" "驗證這是您本人" 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 39900a6f0b..58e5b1eb91 100644 --- a/features/home/impl/src/main/res/values-zh/translations.xml +++ b/features/home/impl/src/main/res/values-zh/translations.xml @@ -1,56 +1,56 @@ - "对此 app 禁用电池优化以确保不错过任何通知。" + "请关闭本应用的电池优化设置,确保不错过任何消息通知。" "禁用优化" "通知未送达?" - "通知提示音已升级:更清晰、更快速、干扰更少。" - "我们已更新你的声音" - "你的聊天已被端到端加密自动备份。如果你无法访问所有设备,则需要使用恢复密钥并保留数字身份。" + "您的通知提示音已升级 - 更清晰、更快速、干扰更少。" + "我们已更新您的声音" + "生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。" "获取恢复密钥" - "备份聊天" + "设置恢复" "确认恢复密钥,以保持对密钥存储和消息历史的访问。" "输入恢复密钥" "忘记了恢复密钥?" "你的密钥存储已不同步" - "为确保你不会错过重要来电,请更改设置以允许锁屏时的全屏通知。" + "为确保您不会错过重要来电,请更改设置以允许锁屏时的全屏通知。" "提升通话体验" - "聊天" + "全部聊天" "空间" - "你确定要拒绝加入 %1$s 的邀请?" + "您确定要拒绝加入 %1$s 的邀请吗?" "拒绝邀请" - "你确定要拒绝与 %1$s 私聊?" + "您确定要拒绝与 %1$s 开始私聊吗?" "拒绝聊天" "没有邀请" - "%1$s(%2$s)邀请了你" - "此为一次性流程,感谢等待。" - "设置账户。" - "创建新的对话或房间" + "%1$s (%2$s)邀请了你" + "这是一个一次性的过程,感谢您的等待。" + "设置您的账户。" + "创建新的对话或聊天室" "清除筛选条件" "通过向某人发送消息来开始。" - "暂无聊天。" - "收藏" - "可以在聊天设置里将聊天添加到收藏夹。 -现在可以取消选择筛选器以查看其它对话。" - "你尚未收藏任何聊天" + "还没有聊天。" + "收藏夹" + "可以在聊天设置里将聊天添加到收藏夹中。 +现在,可以取消选择过滤器以查看其他对话。" + "您未收藏任何聊天" "邀请" "没有待处理的邀请。" "低优先级" - "你暂无任何低优先级聊天" - "你可以取消选择筛选器以查看其它对话" - "你暂无适用于此选项的聊天" - "人员" - "你暂无任何私聊" - "房间" - "你尚未进入任何房间" + "您还没有任何低优先级聊天" + "您可以取消选择过滤器以查看其他对话" + "您没有关于此选项的聊天" + "用户" + "目前您还没有私信" + "聊天室" + "您尚未进入任何聊天室" "未读" "恭喜! 没有任何未读消息!" - "加入申请已发送" - "聊天" - "设为已读" - "设为未读" + "加入请求已发送" + "全部聊天" + "标记为已读" + "标记为未读" "此房间已升级" - "你的空间" - "你似乎正在使用新设备。使用另一台设备进行验证以访问加密消息。" + "您的空间" + "您似乎正在使用新设备。使用另一台设备进行验证以访问您的加密消息。" "验证是你本人" 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 6df5e05df5..1ce5061356 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,9 +14,6 @@ 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 @@ -103,169 +100,11 @@ 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, @@ -273,6 +112,6 @@ class RoomListDataSourceTest { notificationSettingsService = notificationSettingsService, sessionCoroutineScope = backgroundScope, dateTimeObserver = dateTimeObserver, - analyticsService = analyticsService, + analyticsService = FakeAnalyticsService(), ) } 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 de5760c1bd..4c361b47f3 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,13 +6,10 @@ * 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.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.home.impl.R import io.element.android.features.home.impl.filters.selection.FilterSelectionState @@ -20,20 +17,23 @@ 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 org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomListFiltersViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on filters generates expected Event`() = runAndroidComposeUiTest { + fun `clicking on filters generates expected Event`() { val eventsRecorder = EventsRecorder() - setContent { + rule.setContent { RoomListFiltersView( state = aRoomListFiltersState(eventSink = eventsRecorder), ) } - clickOn(R.string.screen_roomlist_filter_rooms) + rule.clickOn(R.string.screen_roomlist_filter_rooms) eventsRecorder.assertList( listOf( RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms), @@ -42,9 +42,9 @@ class RoomListFiltersViewTest { } @Test - fun `clicking on clear filters generates expected Event`() = runAndroidComposeUiTest { + fun `clicking on clear filters generates expected Event`() { val eventsRecorder = EventsRecorder() - setContent { + rule.setContent { RoomListFiltersView( state = aRoomListFiltersState( filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }, @@ -52,7 +52,7 @@ class RoomListFiltersViewTest { ), ) } - pressTag(TestTags.homeScreenClearFilters.value) + rule.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 5fa2adf9d6..6be5fe4c16 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.home.impl.R import io.element.android.libraries.matrix.api.core.RoomId @@ -23,20 +20,23 @@ 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.setSafeContent +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomListContextMenuTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on Mark as read generates expected Events`() = runAndroidComposeUiTest { + fun `clicking on Mark as read generates expected Events`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(hasNewContent = true) - setRoomListContextMenu( + rule.setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, ) - clickOn(R.string.screen_roomlist_mark_as_read) + rule.clickOn(R.string.screen_roomlist_mark_as_read) eventsRecorder.assertList( listOf( RoomListEvent.HideContextMenu, @@ -46,14 +46,14 @@ class RoomListContextMenuTest { } @Test - fun `clicking on Mark as unread generates expected Events`() = runAndroidComposeUiTest { + fun `clicking on Mark as unread generates expected Events`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(hasNewContent = false) - setRoomListContextMenu( + rule.setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, ) - clickOn(R.string.screen_roomlist_mark_as_unread) + rule.clickOn(R.string.screen_roomlist_mark_as_unread) eventsRecorder.assertList( listOf( RoomListEvent.HideContextMenu, @@ -63,14 +63,14 @@ class RoomListContextMenuTest { } @Test - fun `clicking on Leave room generates expected Events`() = runAndroidComposeUiTest { + fun `clicking on Leave room generates expected Events`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(isDm = false) - setRoomListContextMenu( + rule.setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, ) - clickOn(CommonStrings.action_leave_room) + rule.clickOn(CommonStrings.action_leave_room) eventsRecorder.assertList( listOf( RoomListEvent.HideContextMenu, @@ -80,48 +80,48 @@ class RoomListContextMenuTest { } @Test - fun `clicking on Report room invokes the expected callback and generates expected Event`() = runAndroidComposeUiTest { + fun `clicking on Report room invokes the expected callback and generates expected Event`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown() val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) - setRoomListContextMenu( + rule.setRoomListContextMenu( contextMenu = contextMenu, canReportRoom = true, eventSink = eventsRecorder, onRoomSettingsClick = EnsureNeverCalledWithParam(), onReportRoomClick = callback, ) - clickOn(CommonStrings.action_report_room) + rule.clickOn(CommonStrings.action_report_room) eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) callback.assertSuccess() } @Test - fun `clicking on Settings invokes the expected callback and generates expected Event`() = runAndroidComposeUiTest { + fun `clicking on Settings invokes the expected callback and generates expected Event`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown() val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) - setRoomListContextMenu( + rule.setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, onRoomSettingsClick = callback, ) - clickOn(CommonStrings.common_settings) + rule.clickOn(CommonStrings.common_settings) eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) callback.assertSuccess() } @Test - fun `clicking on Favourites generates expected Event`() = runAndroidComposeUiTest { + fun `clicking on Favourites generates expected Event`() { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(isDm = false, isFavorite = false) val callback = EnsureNeverCalledWithParam() - setRoomListContextMenu( + rule.setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, onRoomSettingsClick = callback, ) - clickOn(CommonStrings.common_favourite) + rule.clickOn(CommonStrings.common_favourite) eventsRecorder.assertList( listOf( RoomListEvent.SetRoomIsFavorite(contextMenu.roomId, true), @@ -129,7 +129,7 @@ class RoomListContextMenuTest { ) } - private fun AndroidComposeUiTest.setRoomListContextMenu( + private fun AndroidComposeTestRule<*, *>.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 c8bba05e52..d7f509fda4 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,12 +6,10 @@ * 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.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.home.impl.model.aRoomListRoomSummary import io.element.android.libraries.ui.strings.CommonStrings @@ -20,16 +18,19 @@ 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.setSafeContent +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomListDeclineInviteMenuTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on decline emits the expected Events`() = runAndroidComposeUiTest { + fun `clicking on decline emits the expected Events`() { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - setSafeContent { + rule.setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -37,7 +38,7 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - clickOn(CommonStrings.action_decline) + rule.clickOn(CommonStrings.action_decline) eventsRecorder.assertList( listOf( RoomListEvent.HideDeclineInviteMenu, @@ -47,10 +48,10 @@ class RoomListDeclineInviteMenuTest { } @Test - fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() = runAndroidComposeUiTest { + fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - setSafeContent { + rule.setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = true, @@ -58,16 +59,16 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - clickOn(CommonStrings.action_decline_and_block) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - setSafeContent { + rule.setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -75,7 +76,7 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - clickOn(CommonStrings.action_decline_and_block) + rule.clickOn(CommonStrings.action_decline_and_block) val expectedEvents = listOf( RoomListEvent.HideDeclineInviteMenu, RoomListEvent.DeclineInvite(menu.roomSummary, blockUser = true), @@ -84,10 +85,10 @@ class RoomListDeclineInviteMenuTest { } @Test - fun `clicking on cancel emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on cancel emits the expected Event`() { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - setSafeContent { + rule.setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -95,7 +96,7 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - clickOn(CommonStrings.action_cancel) + rule.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/RoomListViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt index b8d61994fa..8402a921ca 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,19 +6,16 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.home.impl.HomeView import io.element.android.features.home.impl.R @@ -35,17 +32,22 @@ 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.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() + @Config(qualifiers = "h1024dp") @Test - fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() = runAndroidComposeUiTest { + fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() { val eventsRecorder = EventsRecorder() - setRoomListView( + rule.setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, @@ -60,9 +62,9 @@ class RoomListViewTest { } @Test - fun `clicking on close recovery key banner emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on close recovery key banner emits the expected Event`() { val eventsRecorder = EventsRecorder() - setRoomListView( + rule.setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, @@ -72,15 +74,15 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - val close = activity!!.getString(CommonStrings.action_close) - onNodeWithContentDescription(close).performClick() + val close = rule.activity.getString(CommonStrings.action_close) + rule.onNodeWithContentDescription(close).performClick() eventsRecorder.assertSingle(RoomListEvent.DismissBanner) } @Test - fun `clicking on close setup key banner emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on close setup key banner emits the expected Event`() { val eventsRecorder = EventsRecorder() - setRoomListView( + rule.setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery), eventSink = eventsRecorder, @@ -90,16 +92,16 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - val close = activity!!.getString(CommonStrings.action_close) - onNodeWithContentDescription(close).performClick() + val close = rule.activity.getString(CommonStrings.action_close) + rule.onNodeWithContentDescription(close).performClick() eventsRecorder.assertSingle(RoomListEvent.DismissBanner) } @Test - fun `clicking on continue recovery key banner invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on continue recovery key banner invokes the expected callback`() { val eventsRecorder = EventsRecorder() ensureCalledOnce { callback -> - setRoomListView( + rule.setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, @@ -110,17 +112,17 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertEmpty() } } @Test - fun `clicking on continue setup key banner invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on continue setup key banner invokes the expected callback`() { val eventsRecorder = EventsRecorder() ensureCalledOnce { callback -> - setRoomListView( + rule.setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery), eventSink = eventsRecorder, @@ -129,28 +131,28 @@ class RoomListViewTest { ) // Remove automatic initial events eventsRecorder.clear() - clickOn(R.string.banner_set_up_recovery_submit) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on start chat when the session has no room invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setRoomListView( + rule.setRoomListView( state = aRoomListState( eventSink = eventsRecorder, contentState = anEmptyContentState(), ), onCreateRoomClick = callback, ) - clickOn(CommonStrings.action_start_chat) + rule.clickOn(CommonStrings.action_start_chat) } } @Test - fun `clicking on a room invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on a room invokes the expected callback`() { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -159,7 +161,7 @@ class RoomListViewTest { it.displayType == RoomSummaryDisplayType.ROOM } ensureCalledOnceWithParam(room0.roomId) { callback -> - setRoomListView( + rule.setRoomListView( state = state, onRoomClick = callback, ) @@ -167,14 +169,14 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - onNodeWithText(room0.latestEvent.content().toString()).performClick() + rule.onNodeWithText(room0.latestEvent.content().toString()).performClick() } eventsRecorder.assertEmpty() } @Test - fun `clicking on a room twice invokes the expected callback only once`() = runAndroidComposeUiTest { + fun `clicking on a room twice invokes the expected callback only once`() { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -183,13 +185,13 @@ class RoomListViewTest { it.displayType == RoomSummaryDisplayType.ROOM } ensureCalledOnceWithParam(room0.roomId) { callback -> - setRoomListView( + rule.setRoomListView( state = state, onRoomClick = callback, ) // Remove automatic initial events eventsRecorder.clear() - onNodeWithText(room0.latestEvent.content().toString()) + rule.onNodeWithText(room0.latestEvent.content().toString()) .performClick() .performClick() } @@ -197,7 +199,7 @@ class RoomListViewTest { } @Test - fun `long clicking on a room emits the expected Event`() = runAndroidComposeUiTest { + fun `long clicking on a room emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -205,18 +207,18 @@ class RoomListViewTest { val room0 = state.contentAsRooms().summaries.first { it.displayType == RoomSummaryDisplayType.ROOM } - setRoomListView( + rule.setRoomListView( state = state, ) // Remove automatic initial events eventsRecorder.clear() - onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() } + rule.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`() = runAndroidComposeUiTest { + fun `clicking on a room setting invokes the expected callback and emits expected Event`() { val eventsRecorder = EventsRecorder() val state = aRoomListState( contextMenu = aContextMenuShown(), @@ -224,7 +226,7 @@ class RoomListViewTest { ) val room0 = (state.contextMenu as RoomListState.ContextMenu.Shown).roomId ensureCalledOnceWithParam(room0) { callback -> - setRoomListView( + rule.setRoomListView( state = state, onRoomSettingsClick = callback, ) @@ -232,14 +234,14 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - clickOn(CommonStrings.common_settings) + rule.clickOn(CommonStrings.common_settings) } eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) } @Test - fun `clicking on accept and decline invite emits the expected Events`() = runAndroidComposeUiTest { + fun `clicking on accept and decline invite emits the expected Events`() { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -247,13 +249,13 @@ class RoomListViewTest { val invitedRoom = state.contentAsRooms().summaries.first { it.displayType == RoomSummaryDisplayType.INVITE } - setRoomListView(state = state) + rule.setRoomListView(state = state) // Remove automatic initial events eventsRecorder.clear() - clickOn(CommonStrings.action_accept) - clickOn(CommonStrings.action_decline) + rule.clickOn(CommonStrings.action_accept) + rule.clickOn(CommonStrings.action_decline) eventsRecorder.assertList( listOf( RoomListEvent.AcceptInvite(invitedRoom), @@ -263,7 +265,7 @@ class RoomListViewTest { } } -private fun AndroidComposeUiTest.setRoomListView( +private fun AndroidComposeTestRule.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 d612d765b6..5c1325b107 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,32 +5,34 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.tests.testutils.EventsRecorder +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SpaceFiltersViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `clicking on a filter with alias shows display name and alias`() = runAndroidComposeUiTest { + fun `clicking on a filter with alias shows display name and alias`() { val filter = aSpaceServiceFilter( displayName = "Test Space", canonicalAlias = A_ROOM_ALIAS, ) val eventsRecorder = EventsRecorder() - setSpaceFiltersView( + rule.setSpaceFiltersView( state = aSelectingSpaceFiltersState( availableFilters = listOf(filter), eventSink = eventsRecorder, @@ -38,20 +40,20 @@ class SpaceFiltersViewTest { ) // Both display name and alias should be visible - onNodeWithText(filter.spaceRoom.displayName).assertExists() - onNodeWithText(A_ROOM_ALIAS.value).assertExists() + rule.onNodeWithText(filter.spaceRoom.displayName).assertExists() + rule.onNodeWithText(A_ROOM_ALIAS.value).assertExists() - onNodeWithText(filter.spaceRoom.displayName).performClick() + rule.onNodeWithText(filter.spaceRoom.displayName).performClick() eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter)) } @Test - fun `multiple filters are displayed and clickable`() = runAndroidComposeUiTest { + fun `multiple filters are displayed and clickable`() { val filter1 = aSpaceServiceFilter(displayName = "Space One") val filter2 = aSpaceServiceFilter(displayName = "Space Two") val eventsRecorder = EventsRecorder() - setSpaceFiltersView( + rule.setSpaceFiltersView( state = aSelectingSpaceFiltersState( availableFilters = listOf(filter1, filter2), eventSink = eventsRecorder, @@ -59,17 +61,17 @@ class SpaceFiltersViewTest { ) // Both filters should be visible - onNodeWithText(filter1.spaceRoom.displayName).assertExists() - onNodeWithText(filter2.spaceRoom.displayName).assertExists() + rule.onNodeWithText(filter1.spaceRoom.displayName).assertExists() + rule.onNodeWithText(filter2.spaceRoom.displayName).assertExists() // Click on second filter - onNodeWithText(filter2.spaceRoom.displayName).performClick() + rule.onNodeWithText(filter2.spaceRoom.displayName).performClick() eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter2)) } } -private fun AndroidComposeUiTest.setSpaceFiltersView( +private fun AndroidComposeTestRule.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 8bfea2c12c..696e02a0d7 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,6 +11,7 @@ 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 e033f2740c..80b98464f7 100644 --- a/features/invite/impl/build.gradle.kts +++ b/features/invite/impl/build.gradle.kts @@ -33,7 +33,6 @@ 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 db2e76c1c3..3f8bf93afa 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,7 +15,6 @@ 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 { @@ -27,7 +26,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-zh/translations.xml b/features/invite/impl/src/main/res/values-zh/translations.xml index 02825bca6d..8c27397225 100644 --- a/features/invite/impl/src/main/res/values-zh/translations.xml +++ b/features/invite/impl/src/main/res/values-zh/translations.xml @@ -1,18 +1,18 @@ - "你将不会看到来自该用户的任何消息或房间邀请" + "您将不会看到来自该用户的任何信息或房间邀请" "屏蔽用户" - "向账户提供者举报此房间。" - "描述举报的理由…" + "向您的帐户提供商举报此房间。" + "描述举报的原因…" "拒绝并屏蔽" - "你确定要拒绝加入 %1$s 的邀请?" + "您确定要拒绝加入 %1$s 的邀请吗?" "拒绝邀请" - "你确定要拒绝与 %1$s 私聊?" + "您确定要拒绝与 %1$s 开始私聊吗?" "拒绝聊天" "没有邀请" - "%1$s(%2$s)邀请了你" - "是,拒绝并屏蔽" - "你确定要拒绝此房间的加入邀请?这也将阻止 %1$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 e915696de4..299fec8565 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.performTextInput -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.invite.impl.R import io.element.android.libraries.ui.strings.CommonStrings @@ -24,94 +21,98 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class DeclineAndBlockViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invoke the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setDeclineAndBlockView( + rule.setDeclineAndBlockView( aDeclineAndBlockState( eventSink = eventsRecorder, ), onBackClick = it ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on decline when enabled emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on decline when enabled emits the expected event`() { val eventsRecorder = EventsRecorder() - setDeclineAndBlockView( + rule.setDeclineAndBlockView( aDeclineAndBlockState( blockUser = true, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_decline) + rule.clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline) } @Test - fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest { + fun `clicking on decline when disabled does not emit event`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setDeclineAndBlockView( + rule.setDeclineAndBlockView( aDeclineAndBlockState( blockUser = false, reportRoom = false, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_decline) + rule.clickOn(CommonStrings.action_decline) eventsRecorder.assertEmpty() } @Test - fun `clicking on block option emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on block option emits the expected event`() { val eventsRecorder = EventsRecorder() - setDeclineAndBlockView( + rule.setDeclineAndBlockView( aDeclineAndBlockState( blockUser = true, eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_decline_and_block_block_user_option_title) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on report room option emits the expected event`() { val eventsRecorder = EventsRecorder() - setDeclineAndBlockView( + rule.setDeclineAndBlockView( aDeclineAndBlockState( reportRoom = true, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_report_room) + rule.clickOn(CommonStrings.action_report_room) eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom) } @Test - fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest { + fun `typing text in the reason field emits the expected Event`() { val eventsRecorder = EventsRecorder() - setDeclineAndBlockView( + rule.setDeclineAndBlockView( aDeclineAndBlockState( reportRoom = true, reportReason = "", eventSink = eventsRecorder, ), ) - onNodeWithText("").performTextInput("Spam!") + rule.onNodeWithText("").performTextInput("Spam!") eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!")) } } -private fun AndroidComposeUiTest.setDeclineAndBlockView( +private fun AndroidComposeTestRule.setDeclineAndBlockView( state: DeclineAndBlockState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/invite/test/build.gradle.kts b/features/invite/test/build.gradle.kts index 080ed765bb..2df267f155 100644 --- a/features/invite/test/build.gradle.kts +++ b/features/invite/test/build.gradle.kts @@ -16,7 +16,6 @@ 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 0422fac4f1..264aafd570 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,5 +11,4 @@ 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 d14042cff7..9d342d191f 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,12 +9,10 @@ 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 b233ed07ef..ce30bcc1f6 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,7 +10,6 @@ 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 @@ -26,7 +25,6 @@ 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 @@ -34,12 +32,10 @@ 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 2ab2fcb4a3..390ccce7b9 100644 --- a/features/invitepeople/impl/build.gradle.kts +++ b/features/invitepeople/impl/build.gradle.kts @@ -33,10 +33,8 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) - 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 44627daca3..58b3fb67f6 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -36,19 +37,16 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.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 @@ -76,6 +74,7 @@ class DefaultInvitePeoplePresenter( private val coroutineDispatchers: CoroutineDispatchers, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val appErrorStateService: AppErrorStateService, + private val featureFlagService: FeatureFlagService, private val matrixClient: MatrixClient, ) : InvitePeoplePresenter { @AssistedFactory @@ -93,7 +92,8 @@ 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 enableKeyShareOnInvite by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false) val recentDirectRooms by produceState(emptyList(), roomMembers.value) { if (roomMembers.value.isSuccess()) { @@ -137,7 +137,12 @@ class DefaultInvitePeoplePresenter( val selectedUserIdentities = produceState( emptyMap().toImmutableMap(), selectedUsers.value, + enableKeyShareOnInvite, ) { + if (!enableKeyShareOnInvite) { + return@produceState + } + val selected = selectedUsers.value val cached = value @@ -208,19 +213,13 @@ class DefaultInvitePeoplePresenter( } } is InvitePeopleEvents.SendInvites -> { - if (unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) { + if (enableKeyShareOnInvite && unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) { sendInvitesAction.value = ConfirmingUnknownUserInvitation( unknownUsers ) } else { room.dataOrNull()?.let { - sessionCoroutineScope.launch { - if (it.isDm()) { - createRoomFromDm(it, selectedUsers.value, createRoomFromDmAction) - } else { - sendInvites(it, selectedUsers.value, sendInvitesAction) - } - } + sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction) } } } @@ -228,10 +227,6 @@ class DefaultInvitePeoplePresenter( searchActive = false queryState.clearText() } - is InvitePeopleEvents.ClearError -> { - sendInvitesAction.value = AsyncAction.Uninitialized - createRoomFromDmAction.value = AsyncAction.Uninitialized - } } } @@ -244,7 +239,6 @@ class DefaultInvitePeoplePresenter( searchResults = searchResults.value, showSearchLoader = showSearchLoader.value, sendInvitesAction = sendInvitesAction.value, - createRoomFromDmAction = createRoomFromDmAction.value, suggestions = suggestions, eventSink = ::handleEvent, ) @@ -271,35 +265,6 @@ 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 46e5d9f1a5..842bcf1148 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,7 +14,6 @@ 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 @@ -27,7 +26,6 @@ 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 93a6e03bd3..c26b8de254 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt @@ -12,13 +12,7 @@ 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 @@ -39,15 +33,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) }, @@ -134,7 +125,6 @@ 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 2bbd64c977..38db9d55be 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,7 +14,6 @@ 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 @@ -24,7 +23,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.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -56,10 +55,7 @@ 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 @Composable @@ -106,7 +102,7 @@ private fun InvitePeopleContentView( } InvitePeopleSearchBar( - modifier = Modifier.imePadding().fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), queryState = state.searchQuery, showLoader = state.showSearchLoader, selectedUsers = state.selectedUsers, @@ -266,19 +262,10 @@ private fun InvitePeopleConfirmModal( ModalBottomSheet( onDismissRequest = onDismiss, dragHandle = null, - scrollable = false, ) { IconTitleSubtitleMolecule( - title = simplePluralStringResource( - resIdForOne = R.string.screen_invite_users_confirm_dialog_title_one_user, - resIdForOthers = R.string.screen_invite_users_confirm_dialog_title_mutiple_users, - count = users.size, - ), - subTitle = simplePluralStringResource( - resIdForOne = R.string.screen_invite_users_confirm_dialog_subtitle_one_user, - resIdForOthers = R.string.screen_invite_users_confirm_dialog_subtitle_multiple_users, - count = users.size, - ), + title = pluralStringResource(R.plurals.screen_invite_users_confirm_dialog_title, users.size), + subTitle = pluralStringResource(R.plurals.screen_invite_users_confirm_dialog_subtitle, users.size), iconStyle = BigIcon.Style.Default(CompoundIcons.UserAddSolid()), modifier = Modifier.padding( top = 32.dp, @@ -302,7 +289,7 @@ private fun InvitePeopleConfirmModal( text = stringResource(CommonStrings.action_remove), onClick = onRemove, leadingIcon = IconSource.Vector(CompoundIcons.Close()), - modifier = Modifier.weight(1f).testTag(TestTags.confirmInviteUnknown), + modifier = Modifier.weight(1f) ) 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 deleted file mode 100644 index 294d04b4a3..0000000000 --- a/features/invitepeople/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "Ja és membre" - "Ja s\'ha convidat" - diff --git a/features/invitepeople/impl/src/main/res/values-cs/translations.xml b/features/invitepeople/impl/src/main/res/values-cs/translations.xml index c041433267..fa5b3aa9a9 100644 --- a/features/invitepeople/impl/src/main/res/values-cs/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-cs/translations.xml @@ -2,8 +2,4 @@ "Již členem" "Již pozván(a)" - "Momentálně s těmito kontakty nemáte žádné chaty. Před pokračováním potvrďte jejich pozvání do této místnosti." - "Momentálně s tímto kontaktem nemáte žádné chaty. Před pokračováním potvrďte pozvání do této místnosti." - "Pozvat nové kontakty do této místnosti?" - "Pozvat nový kontakt do této místnosti?" 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 1754ef6e0d..fbb1814e9f 100644 --- a/features/invitepeople/impl/src/main/res/values-da/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-da/translations.xml @@ -2,8 +2,4 @@ "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 1bdf5f780c..44484d23c3 100644 --- a/features/invitepeople/impl/src/main/res/values-et/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-et/translations.xml @@ -2,8 +2,4 @@ "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 d2283c7b2a..e347919719 100644 --- a/features/invitepeople/impl/src/main/res/values-fi/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-fi/translations.xml @@ -2,8 +2,4 @@ "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-fr/translations.xml b/features/invitepeople/impl/src/main/res/values-fr/translations.xml index b5df870002..dcc16f58cf 100644 --- a/features/invitepeople/impl/src/main/res/values-fr/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-fr/translations.xml @@ -2,8 +2,4 @@ "Déjà membre" "Déjà invité(e)" - "Vous n’avez actuellement aucune conversation avec ces contacts. Veuillez confirmer leur invitation à rejoindre ce salon avant de continuer." - "Vous n’avez actuellement aucune conversation avec ce contact. Veuillez confirmer son invitation à rejoindre ce salon avant de continuer." - "Inviter de nouveaux contacts dans ce salon ?" - "Inviter un nouveau contact dans ce salon ?" 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 471b79d709..66031c5fd7 100644 --- a/features/invitepeople/impl/src/main/res/values-hr/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-hr/translations.xml @@ -2,8 +2,4 @@ "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 de2cab0d73..16f35b018c 100644 --- a/features/invitepeople/impl/src/main/res/values-hu/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-hu/translations.xml @@ -2,8 +2,4 @@ "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 82dc6126f1..979e42de1b 100644 --- a/features/invitepeople/impl/src/main/res/values-it/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-it/translations.xml @@ -2,8 +2,4 @@ "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-ja/translations.xml b/features/invitepeople/impl/src/main/res/values-ja/translations.xml index db5b91ca2b..6aa1fb0370 100644 --- a/features/invitepeople/impl/src/main/res/values-ja/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-ja/translations.xml @@ -2,8 +2,10 @@ "既に参加しています" "既に招待しています" - "これらの人物とのチャットがありません。はじめに、招待の状況を確認してください。" - "この連絡先とのチャットがありません。はじめに、招待の状況を確認してください。" - "このルームに新しい連絡先を招待しますか?" - "このルームに新しい連絡先を招待しますか?" + + "この連絡先とのチャットがありません。続行する前に、このルームに招待してください。" + + + "このルームに新しい連絡先を追加しますか?" + 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 3e8371c1ab..bfd537bb4b 100644 --- a/features/invitepeople/impl/src/main/res/values-pl/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-pl/translations.xml @@ -2,8 +2,4 @@ "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 40189e3186..f03be4b263 100644 --- a/features/invitepeople/impl/src/main/res/values-ro/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-ro/translations.xml @@ -2,8 +2,4 @@ "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 1f2455ba72..45e650f081 100644 --- a/features/invitepeople/impl/src/main/res/values-ru/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-ru/translations.xml @@ -2,8 +2,4 @@ "Уже участник" "Уже приглашен(а)" - "У тебя пока нет чатов с этими контактами. Подтверди приглашение в эту комнату, прежде чем продолжить." - "У вас пока нет чатов с этим контактом. Подтверди приглашение в эту комнату, прежде чем продолжить." - "Пригласить новых участников в эту комнату?" - "Пригласить нового участника в эту комнату?" 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 b7bb3c95d9..da3ac9fe5b 100644 --- a/features/invitepeople/impl/src/main/res/values-uk/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-uk/translations.xml @@ -2,8 +2,4 @@ "Уже учасник" "Уже запрошені" - "Наразі у вас немає чатів із цими контактами. Підтвердьте запрошення їх до цієї кімнати, перш ніж продовжувати." - "Наразі у вас немає чатів із цим контактом. Підтвердьте запрошення до цієї кімнати, перш ніж продовжувати." - "Запросити нових контактів до цієї кімнати?" - "Запросити нового контакта до цієї кімнати?" diff --git a/features/invitepeople/impl/src/main/res/values-zh/translations.xml b/features/invitepeople/impl/src/main/res/values-zh/translations.xml index 7b1bb29288..b1e0e953f8 100644 --- a/features/invitepeople/impl/src/main/res/values-zh/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-zh/translations.xml @@ -2,8 +2,4 @@ "已经是成员" "已邀请" - "你与这些联系人暂无任何聊天。请确认对方被邀请到此房间后再继续。" - "你与此人暂无任何聊天。请确认对方被邀请到此房间后再继续。" - "邀请新联系人到此房间?" - "邀请新联系人到此房间?" diff --git a/features/invitepeople/impl/src/main/res/values/localazy.xml b/features/invitepeople/impl/src/main/res/values/localazy.xml index aae71fe4c2..0515121428 100644 --- a/features/invitepeople/impl/src/main/res/values/localazy.xml +++ b/features/invitepeople/impl/src/main/res/values/localazy.xml @@ -2,8 +2,12 @@ "Already a member" "Already invited" - "You currently don’t have any chats with these contacts. Confirm inviting them to this room before continuing." - "You currently don’t have any chats with this contact. Confirm inviting them to this room before continuing." - "Invite new contacts to this room?" - "Invite new contact to this room?" + + "You currently don’t have any chats with this contact. Confirm inviting them to this room before continuing." + "You currently don’t have any chats with these contacts. Confirm inviting them to this room before continuing." + + + "Invite a new contact to this room?" + "Invite new contacts to this room?" + diff --git a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt index 4e141b2c4d..a1d72010f6 100644 --- a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt +++ b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt @@ -15,6 +15,9 @@ import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -320,7 +323,7 @@ internal class DefaultInvitePeoplePresenterTest { val initialState = awaitItemAsDefault() skipItems(1) - val selectedUser = aMatrixUser(displayName = "John Doe") + val selectedUser = aMatrixUser() initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser)) @@ -358,7 +361,7 @@ internal class DefaultInvitePeoplePresenterTest { val initialState = awaitItemAsDefault() skipItems(1) - val selectedUser = aMatrixUser(displayName = "John Doe") + val selectedUser = aMatrixUser() // Given a query is made initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query") @@ -402,14 +405,10 @@ internal class DefaultInvitePeoplePresenterTest { val inviteUserResult = lambdaRecorder> { userId: UserId -> Result.success(Unit) } - val encryptionService = FakeEncryptionService( - getUserIdentityResult = { _ -> Result.success(null) }, - ) val presenter = createDefaultInvitePeoplePresenter( userRepository = repository, inviteUserResult = inviteUserResult, - coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), - matrixClient = FakeMatrixClient(encryptionService = encryptionService), + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) presenter.test { val initialState = awaitItem() @@ -452,18 +451,13 @@ internal class DefaultInvitePeoplePresenterTest { Result.failure(AN_EXCEPTION) } val showErrorResResult = lambdaRecorder { _, _ -> } - - val encryptionService = FakeEncryptionService( - getUserIdentityResult = { _ -> Result.success(null) }, - ) val presenter = createDefaultInvitePeoplePresenter( userRepository = repository, inviteUserResult = inviteUserResult, coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), appErrorStateService = FakeAppErrorStateService( showErrorResResult = showErrorResResult, - ), - matrixClient = FakeMatrixClient(encryptionService = encryptionService), + ) ) presenter.test { val initialState = awaitItem() @@ -540,7 +534,7 @@ internal class DefaultInvitePeoplePresenterTest { } @Test - fun `present - suggestions are loaded from recent DM rooms`() = runTest { + fun `present - suggestions are loaded from recent direct 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 +548,7 @@ internal class DefaultInvitePeoplePresenterTest { roomId = dmRoomId, initialRoomInfo = aRoomInfo( id = dmRoomId, - isDm = true, + isDirect = true, activeMembersCount = 2, currentUserMembership = CurrentUserMembership.JOINED, ), @@ -591,7 +585,7 @@ internal class DefaultInvitePeoplePresenterTest { roomId = dmRoomId, initialRoomInfo = aRoomInfo( id = dmRoomId, - isDm = true, + isDirect = true, activeMembersCount = 2, currentUserMembership = CurrentUserMembership.JOINED, ), @@ -638,11 +632,15 @@ internal class DefaultInvitePeoplePresenterTest { val encryptionService = FakeEncryptionService( getUserIdentityResult = getUserIdentityResult ) + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } val presenter = createDefaultInvitePeoplePresenter( coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), inviteUserResult = inviteUserResult, matrixClient = FakeMatrixClient(encryptionService = encryptionService), + featureFlagService = featureFlagService ) presenter.test { val initialState = awaitItem() @@ -705,11 +703,15 @@ internal class DefaultInvitePeoplePresenterTest { val encryptionService = FakeEncryptionService( getUserIdentityResult = getUserIdentityResult ) + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } val presenter = createDefaultInvitePeoplePresenter( userRepository = repository, coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), matrixClient = FakeMatrixClient(encryptionService = encryptionService), + featureFlagService = featureFlagService ) presenter.test { val initialState = awaitItemAsDefault() @@ -788,10 +790,14 @@ internal class DefaultInvitePeoplePresenterTest { val encryptionService = FakeEncryptionService( getUserIdentityResult = getUserIdentityResult ) + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } val presenter = createDefaultInvitePeoplePresenter( coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), matrixClient = FakeMatrixClient(encryptionService = encryptionService), + featureFlagService = featureFlagService ) presenter.test { val initialState = awaitItem() @@ -831,54 +837,6 @@ 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 @@ -920,6 +878,7 @@ fun TestScope.createDefaultInvitePeoplePresenter( userRepository: UserRepository = FakeUserRepository(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), appErrorStateService: AppErrorStateService = FakeAppErrorStateService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), matrixClient: MatrixClient = FakeMatrixClient(), ): DefaultInvitePeoplePresenter { return DefaultInvitePeoplePresenter( @@ -929,6 +888,7 @@ fun TestScope.createDefaultInvitePeoplePresenter( coroutineDispatchers = coroutineDispatchers, sessionCoroutineScope = backgroundScope, appErrorStateService = appErrorStateService, + featureFlagService = featureFlagService, matrixClient = matrixClient, ) } 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 d257952209..1e685d3f5a 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,6 +44,7 @@ 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 4bfa741321..7e5142321a 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,9 +15,6 @@ 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 @@ -137,8 +134,8 @@ open class JoinRoomStateProvider : PreviewParameterProvider { joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned( banSender = InviteSender( userId = UserId("@alice:domain"), - displayName = USER_NAME_ALICE, - avatarData = AvatarData("alice", USER_NAME_ALICE, size = AvatarSize.InviteSender), + displayName = "Alice", + avatarData = AvatarData("alice", "Alice", size = AvatarSize.InviteSender), membershipChangeReason = "spamming" ), reason = "spamming", @@ -225,7 +222,7 @@ fun aJoinRoomState( internal fun anInviteSender( userId: UserId = UserId("@bob:domain"), - displayName: String = USER_NAME_BOB, + displayName: String = "Bob", avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender), membershipChangeReason: String? = null, ) = InviteSender( @@ -237,7 +234,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 977308178d..35cfbb1594 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,7 +609,6 @@ private fun JoinRoomTopBar( val roundedCornerShape = RoundedCornerShape(8.dp) val titleModifier = Modifier .clip(roundedCornerShape) - .semantics { heading() } if (contentState.name != null) { Row( modifier = titleModifier, @@ -622,7 +621,10 @@ private fun JoinRoomTopBar( ) Text( modifier = Modifier - .padding(horizontal = 8.dp), + .padding(horizontal = 8.dp) + .semantics { + heading() + }, text = contentState.name, style = ElementTheme.typography.fontBodyLgMedium, maxLines = 1, diff --git a/features/joinroom/impl/src/main/res/values-ca/translations.xml b/features/joinroom/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index e26c33d40f..0000000000 --- a/features/joinroom/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - "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-uk/translations.xml b/features/joinroom/impl/src/main/res/values-uk/translations.xml index 1a83a6ca8c..ec2a24950a 100644 --- a/features/joinroom/impl/src/main/res/values-uk/translations.xml +++ b/features/joinroom/impl/src/main/res/values-uk/translations.xml @@ -15,7 +15,6 @@ "Вам потрібно отримати запрошення, щоб приєднатися, інакше доступ може бути обмежений." "Забути" "Вам потрібне запрошення, щоб приєднатися" - "Запрошено користувачем" "Доєднатися" "Можливо, вам знадобиться отримати запрошення або стати учасником простору, щоб приєднатися." "Постукати, щоб приєднатися" diff --git a/features/joinroom/impl/src/main/res/values-vi/translations.xml b/features/joinroom/impl/src/main/res/values-vi/translations.xml index bf5c33ba66..cb9258b308 100644 --- a/features/joinroom/impl/src/main/res/values-vi/translations.xml +++ b/features/joinroom/impl/src/main/res/values-vi/translations.xml @@ -1,7 +1,5 @@ - "Bạn đã bị cấm bởi %1$s ." - "Bạn đã bị cấm" "Hủy yêu cầu" "Có, hủy" "Bạn có chắc chắn muốn hủy yêu cầu tham gia phòng này không?" diff --git a/features/joinroom/impl/src/main/res/values-zh/translations.xml b/features/joinroom/impl/src/main/res/values-zh/translations.xml index e4de8dfec4..d38f9d3427 100644 --- a/features/joinroom/impl/src/main/res/values-zh/translations.xml +++ b/features/joinroom/impl/src/main/res/values-zh/translations.xml @@ -1,34 +1,34 @@ - "你已被 %1$s 封禁。" + "您已被 %1$s 封禁。" "你已被此房间封禁" "理由:%1$s。" - "取消申请" - "是,取消" - "你确定要取消加入此房间的申请?" + "取消请求" + "是的,取消" + "您确定要取消加入此房间的请求吗?" "取消加入申请" - "是,拒绝并屏蔽" - "你确定要拒绝此房间的加入邀请?这也将阻止 %1$s 与你联系或邀请你加入房间。" + "是的,拒绝并屏蔽" + "您确定要拒绝加入此房间的邀请吗?这也将阻止 %1$s 与您联系或邀请您加入房间。" "拒绝邀请并屏蔽" "拒绝并屏蔽" "加入失败" - "你需要被邀请才能加入,否则可能会遭遇访问限制。" + "您需要被邀请加入,否则可能会受到访问限制。" "忘记" - "你需要被邀请才能加入" + "您需要邀请才能加入" "受邀于" "加入" - "你可能需要被邀请或成为某个空间的成员才能加入。" - "加入房间" - "允许的字符数量共 %2$d 个,当前为 %1$d 个" + "您可能需要受到邀请或成为某个空间的成员才能加入。" + "加入聊天室" + "允许的字符数量 %2$d中的%1$d" "消息(可选)" - "如果你的申请被批准,你将收到加入房间的邀请。" - "加入申请已发送" + "如果您的请求被接受,您将收到加入房间的邀请。" + "加入请求已发送" "无法显示房间预览。这可能是由于网络或服务器问题造成的。" "无法显示此房间预览" - "%1$s 暂不支持空间。你可以通过 Web 客户端访问空间。" - "空间尚未受到支持" - "点击以下按钮以通知房间管理员。获得批准后你将能加入对话。" - "只有房间成员才能查看消息历史。" - "想加入此房间吗?" + "%1$s 尚不支持空间。您可以通过 Web 端访问空间" + "空间尚不支持" + "点击下面的按钮,系统将通知聊天室管理员。获得批准后将能够加入对话。" + "只有聊天室成员才能查看消息历史记录。" + "想加入此聊天室吗?" "预览不可用" 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 e60d7da691..0a3b1ca3c6 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.test.anInviteData @@ -29,112 +26,116 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class JoinRoomViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invoke the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( eventSink = eventsRecorder, ), onBackClick = it ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on Join room on CanJoin room emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on Join room on CanJoin room emits the expected Event`() { val eventsRecorder = EventsRecorder() - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin), eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_join_room_join_action) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on Knock room on CanKnock room emits the expected Event`() { val eventsRecorder = EventsRecorder() - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), knockMessage = "Knock knock", eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_join_room_knock_action) + rule.clickOn(R.string.screen_join_room_knock_action) eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom) } @Test - fun `clicking on closing Knock error emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on closing Knock error emits the expected Event`() { val eventsRecorder = EventsRecorder() - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), knockAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) } @Test - fun `clicking on cancel knock request emit the expected Event`() = runAndroidComposeUiTest { + fun `clicking on cancel knock request emit the expected Event`() { val eventsRecorder = EventsRecorder() - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked), eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_join_room_cancel_knock_action) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on closing Cancel Knock error emits the expected Event`() { val eventsRecorder = EventsRecorder() - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked), cancelKnockAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) } @Test - fun `clicking on closing Join error emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on closing Join error emits the expected Event`() { val eventsRecorder = EventsRecorder() - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), joinAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) } @Test - fun `when joining room is successful, the expected callback is invoked`() = runAndroidComposeUiTest { + fun `when joining room is successful, the expected callback is invoked`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( joinAction = AsyncAction.Success(Unit), eventSink = eventsRecorder, @@ -145,55 +146,53 @@ class JoinRoomViewTest { } @Test - fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() { val eventsRecorder = EventsRecorder() val inviteData = anInviteData() - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_accept) + rule.clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData)) } @Test - fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() { val eventsRecorder = EventsRecorder() val inviteData = anInviteData() - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_decline) + rule.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`() { - runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - val inviteData = anInviteData() - val joinRoomState = aJoinRoomState( - contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())), - canReportRoom = true, - eventSink = eventsRecorder, + 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, ) - ensureCalledOnceWithParam(inviteData) { - setJoinRoomView( - state = joinRoomState, - onDeclineInviteAndBlockUser = it, - ) - clickOn(R.string.screen_join_room_decline_and_block_button_title) - } + rule.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`() = runAndroidComposeUiTest { + fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() { val eventsRecorder = EventsRecorder() val inviteData = anInviteData() val joinRoomState = aJoinRoomState( @@ -201,29 +200,29 @@ class JoinRoomViewTest { canReportRoom = false, eventSink = eventsRecorder, ) - setJoinRoomView(state = joinRoomState) - clickOn(R.string.screen_join_room_decline_and_block_button_title) + rule.setJoinRoomView(state = joinRoomState) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on Retry when an error occurs emits the expected Event`() { val eventsRecorder = EventsRecorder() - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aFailureContentState(), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_retry) + rule.clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent) } @Test - fun `clicking on ok when user is unauthorized the expected callback`() = runAndroidComposeUiTest { + fun `clicking on ok when user is unauthorized the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(), joinAction = AsyncAction.Failure(JoinRoom.Failures.UnauthorizedJoin), @@ -231,25 +230,25 @@ class JoinRoomViewTest { ), onBackClick = it ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) } } @Test - fun `clicking on forget when user is banned invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on forget when user is banned invokes the expected callback`() { val eventsRecorder = EventsRecorder() - setJoinRoomView( + rule.setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)), eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_join_room_forget_action) + rule.clickOn(R.string.screen_join_room_forget_action) eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom) } } -private fun AndroidComposeUiTest.setJoinRoomView( +private fun AndroidComposeTestRule.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 e6a1a30167..6f030479f5 100644 --- a/features/knockrequests/impl/build.gradle.kts +++ b/features/knockrequests/impl/build.gradle.kts @@ -33,7 +33,9 @@ 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 7210e783fe..67f1aaae8f 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,9 +11,6 @@ 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 { @@ -32,15 +29,15 @@ class KnockRequestsBannerStateProvider : PreviewParameterProvider perms.knockRequestPermissions() }, - coroutineScope = room.roomCoroutineScope, + isKnockFeatureEnabledFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock), + 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 00e0a30563..98570e6b28 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,6 +12,7 @@ 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 @@ -27,20 +28,26 @@ 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, - ) { knockRequests, handledKnockIds -> - val presentableKnockRequests = knockRequests - .filter { it.eventId !in handledKnockIds } - .map { inner -> KnockRequestWrapper(inner) } - .toImmutableList() - AsyncData.Success(presentableKnockRequests) + ) { 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) + } }.stateIn(coroutineScope, SharingStarted.Lazily, AsyncData.Loading()) val permissionsFlow = permissionsFlow.stateIn( diff --git a/features/knockrequests/impl/src/main/res/values-ca/translations.xml b/features/knockrequests/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index bdf1cac21b..0000000000 --- a/features/knockrequests/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - "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-vi/translations.xml b/features/knockrequests/impl/src/main/res/values-vi/translations.xml index ab7c39c2ef..a80aa6fbb8 100644 --- a/features/knockrequests/impl/src/main/res/values-vi/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-vi/translations.xml @@ -24,9 +24,6 @@ "Khi ai đó xin vào phòng, bạn sẽ thấy yêu cầu ở đây." "Không có yêu cầu tham gia nào đang chờ xử lý" "Đang tải các yêu cầu tham gia…" - - "%1$s + %2$d người khác muốn tham gia phòng này" - "Đồng ý" "Xem" diff --git a/features/knockrequests/impl/src/main/res/values-zh/translations.xml b/features/knockrequests/impl/src/main/res/values-zh/translations.xml index d8b8f76b13..08718dfc5a 100644 --- a/features/knockrequests/impl/src/main/res/values-zh/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-zh/translations.xml @@ -1,32 +1,32 @@ - "是,全部接受" - "你确定要接受所有加入申请?" - "接受所有申请" + "是的,全部接受" + "您确定要接受所有加入请求吗?" + "接受所有请求" "全部接受" - "我们无法接受所有申请。是否重试?" - "无法接受所有申请" - "接受所有加入申请" - "我们无法接受此申请。是否重试?" - "无法接受申请" - "接受加入申请" - "是,拒绝并禁止" - "你确定要拒绝并封禁 %1$s?该用户将无法再次申请加入该房间。" + "我们无法接受所有请求。是否要再试一次?" + "无法接受所有请求" + "接受所有加入请求" + "我们无法接受此请求。是否要再试一次?" + "无法接受请求" + "接受加入请求" + "是的,拒绝并禁止" + "您确定要拒绝并禁止吗%1$s?该用户将无法再次请求加入该房间。" "拒绝并禁止访问" "拒绝并禁止访问" - "是,拒绝" - "你确定要拒绝 %1$s 加入此房间的申请?" + "是的,拒绝" + "您确定要拒绝 %1$s 加入此房间的请求吗?" "拒绝访问" - "拒绝并封禁" - "我们无法拒绝此申请。是否重试?" - "拒绝申请失败" - "拒绝加入申请" - "当有人申请加入房间时,你将能够在这里看到其申请。" - "暂无待处理的加入申请" - "正在加载加入申请…" + "拒绝和禁止" + "我们无法拒绝此请求。是否要再试一次?" + "拒绝请求失败" + "拒绝加入请求" + "当有人请求加入房间时,您将能够在这里看到他们的请求。" + "没有待处理的加入请求" + "正在加载加入请求…" "申请加入" - "%1$s、%2$d 及其他人想加入此房间" + "%1$s+ %2$d 其他人想加入这个房间" "查看全部" "接受" 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 1595fcdbd9..3161d3e81f 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,6 +28,18 @@ 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()) @@ -217,10 +229,12 @@ 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 fc1600d8c8..a9fea0905e 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.knockrequests.impl.R import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable @@ -24,30 +21,35 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class KnockRequestsBannerViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `clicking on view on single request invoke the expected callback`() = runAndroidComposeUiTest { + fun `clicking on view on single request invoke the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setKnockRequestsBannerView( + rule.setKnockRequestsBannerView( state = aKnockRequestsBannerState( eventSink = eventsRecorder, ), onViewRequestsClick = it ) - clickOn(R.string.screen_room_single_knock_request_view_button_title) + rule.clickOn(R.string.screen_room_single_knock_request_view_button_title) } } @Test - fun `clicking on view all when multiple requests invoke the expected callback`() = runAndroidComposeUiTest { + fun `clicking on view all when multiple requests invoke the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setKnockRequestsBannerView( + rule.setKnockRequestsBannerView( state = aKnockRequestsBannerState( knockRequests = listOf( aKnockRequestPresentable(displayName = "Alice"), @@ -58,37 +60,37 @@ class KnockRequestsBannerViewTest { ), onViewRequestsClick = it ) - clickOn(R.string.screen_room_multiple_knock_requests_view_all_button_title) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on accept on a single request emit the expected event`() { val eventsRecorder = EventsRecorder() - setKnockRequestsBannerView( + rule.setKnockRequestsBannerView( state = aKnockRequestsBannerState( eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_accept) + rule.clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(KnockRequestsBannerEvents.AcceptSingleRequest) } @Test - fun `clicking on dismiss emit the expected event`() = runAndroidComposeUiTest { + fun `clicking on dismiss emit the expected event`() { val eventsRecorder = EventsRecorder() - setKnockRequestsBannerView( + rule.setKnockRequestsBannerView( state = aKnockRequestsBannerState( eventSink = eventsRecorder, ), ) - val close = activity!!.getString(CommonStrings.action_close) - onNodeWithContentDescription(close).performClick() + val close = rule.activity.getString(CommonStrings.action_close) + rule.onNodeWithContentDescription(close).performClick() eventsRecorder.assertSingle(KnockRequestsBannerEvents.Dismiss) } } -private fun AndroidComposeUiTest.setKnockRequestsBannerView( +private fun AndroidComposeTestRule.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 209e67cadf..7102b01773 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,6 +298,7 @@ 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 14cac7a9b7..188dcc7e56 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.knockrequests.impl.R import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable @@ -26,86 +23,90 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack 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() + @Test - fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invoke the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setKnockRequestsListView( + rule.setKnockRequestsListView( aKnockRequestsListState( eventSink = eventsRecorder, ), onBackClick = it ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on accept emit the expected event`() = runAndroidComposeUiTest { + fun `clicking on accept emit the expected event`() { val eventsRecorder = EventsRecorder() val knockRequest = aKnockRequestPresentable() - setKnockRequestsListView( + rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_accept) + rule.clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(KnockRequestsListEvents.Accept(knockRequest)) } @Test - fun `clicking on decline emit the expected event`() = runAndroidComposeUiTest { + fun `clicking on decline emit the expected event`() { val eventsRecorder = EventsRecorder() val knockRequest = aKnockRequestPresentable() - setKnockRequestsListView( + rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_decline) + rule.clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(KnockRequestsListEvents.Decline(knockRequest)) } @Test - fun `clicking on decline and ban emit the expected event`() = runAndroidComposeUiTest { + fun `clicking on decline and ban emit the expected event`() { val eventsRecorder = EventsRecorder() val knockRequest = aKnockRequestPresentable() - setKnockRequestsListView( + rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on accept all emit the expected event`() { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - setKnockRequestsListView( + rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_knock_requests_list_accept_all_button_title) + rule.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`() = runAndroidComposeUiTest { + fun `retry on async view retry emit the expected event`() { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - setKnockRequestsListView( + rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")), @@ -113,15 +114,15 @@ class KnockRequestsListViewTest { eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_retry) + rule.clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(KnockRequestsListEvents.RetryCurrentAction) } @Test - fun `canceling async view emit the expected event`() = runAndroidComposeUiTest { + fun `canceling async view emit the expected event`() { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - setKnockRequestsListView( + rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")), @@ -129,15 +130,15 @@ class KnockRequestsListViewTest { eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(KnockRequestsListEvents.ResetCurrentAction) } @Test - fun `confirming async view emit the expected event`() = runAndroidComposeUiTest { + fun `confirming async view emit the expected event`() { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - setKnockRequestsListView( + rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.ConfirmingNoParams, @@ -145,12 +146,12 @@ class KnockRequestsListViewTest { eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title) + rule.clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title) eventsRecorder.assertSingle(KnockRequestsListEvents.ConfirmCurrentAction) } } -private fun AndroidComposeUiTest.setKnockRequestsListView( +private fun AndroidComposeTestRule.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 deleted file mode 100644 index 870e0c35f2..0000000000 --- a/features/leaveroom/api/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - "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 ec2cd89314..167070891f 100644 --- a/features/leaveroom/api/src/main/res/values-fa/translations.xml +++ b/features/leaveroom/api/src/main/res/values-fa/translations.xml @@ -1,6 +1,5 @@ - "آیا مطمئنید که می‌خواهید این مکالمه را ترک کنید؟ این مکالمه عمومی نیست و بدون دعوت نمی‌توانید دوباره به آن بپیوندید." "مطمئنید که می‌خواهید این اتاق را ترک کنید؟ تنها فرد این‌جا هستید. در صورت ترک، هیچ‌کسی از جمله خودتان در آینده نخواهد توانست به آن بپیوندد." "مطمئنید که می‌خواهید این اتاق را ترک کنید؟ این اتاق عمومی نبوده قادر نخواهید بود بدون دعوت دوباره بپیوندید." "گزینش مالکان" diff --git a/features/leaveroom/api/src/main/res/values-vi/translations.xml b/features/leaveroom/api/src/main/res/values-vi/translations.xml index 430ffa0ea3..d25a7e34b2 100644 --- a/features/leaveroom/api/src/main/res/values-vi/translations.xml +++ b/features/leaveroom/api/src/main/res/values-vi/translations.xml @@ -3,8 +3,5 @@ "Bạn có chắc chắn muốn rời khỏi cuộc trò chuyện này không? Cuộc trò chuyện này không công khai và bạn sẽ không thể tham gia lại nếu không được mời." "Bạn có chắc chắn muốn rời khỏi phòng này không? Bạn là người duy nhất ở đây. Nếu bạn rời đi, sẽ không ai có thể tham gia nữa, kể cả bạn." "Bạn có chắc chắn muốn rời khỏi phòng này không? Phòng này không công khai và bạn sẽ không thể tham gia lại nếu không có lời mời." - "Chọn chủ sở hữu" - "Bạn là chủ sở hữu duy nhất của căn phòng này. Bạn cần chuyển quyền sở hữu cho người khác trước khi rời khỏi phòng." - "Chuyển quyền sở hữu" "Bạn có chắc chắn muốn rời khỏi phòng không?" diff --git a/features/leaveroom/api/src/main/res/values-zh/translations.xml b/features/leaveroom/api/src/main/res/values-zh/translations.xml index a4c920a221..6b7f17558b 100644 --- a/features/leaveroom/api/src/main/res/values-zh/translations.xml +++ b/features/leaveroom/api/src/main/res/values-zh/translations.xml @@ -1,10 +1,10 @@ - "你确定要离开此对话?此对话不公开,你将无法在未经邀请的情况下重新加入。" - "确定要离开此房间?此处只有你一个人。如果离开,包括你在内的所有人都将无法加入。" - "确定要离开此房间吗?此房间不公开,没有邀请你将无法重新加入。" + "您确定要离开此对话吗?此对话不公开,未经邀请您将无法重新加入。" + "确定要离开此聊天室吗?此处只有你一个人。如果离开此聊天室,包括你在内的所有人都将无法进入。" + "确定要离开此聊天室吗?此聊天室不公开,没有邀请你将无法重新加入。" "选择所有者" - "你是此房间的唯一所有者。离开前需要转让所有权给他人。" + "您是本房间的唯一所有者。离开房间前,您需要将所有权转移给他人。" "转让所有权" - "确定要离开房间?" + "确定要离开聊天室吗?" 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 d11dc7e7e8..6455b45659 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,6 +24,7 @@ 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 90b7f2369f..59d2c1ce23 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(isDm = true, activeMembersCount = 2)) + givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 2)) }, ) } diff --git a/features/linknewdevice/impl/build.gradle.kts b/features/linknewdevice/impl/build.gradle.kts index adbec91e6a..9c1aa9e990 100644 --- a/features/linknewdevice/impl/build.gradle.kts +++ b/features/linknewdevice/impl/build.gradle.kts @@ -43,7 +43,7 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.qrcode) - implementation(projects.libraries.oauth.api) + implementation(projects.libraries.oidc.api) implementation(projects.libraries.uiUtils) implementation(projects.libraries.wellknown.api) implementation(libs.androidx.browser) @@ -56,7 +56,7 @@ dependencies { testImplementation(projects.features.enterprise.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.oauth.test) + testImplementation(projects.libraries.oidc.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 61645ead9d..54baee6663 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 @@ -26,9 +26,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.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 @@ -66,7 +64,6 @@ class LinkNewDeviceFlowNode( private val sessionCoroutineScope: CoroutineScope, private val linkNewMobileHandler: LinkNewMobileHandler, private val linkNewDesktopHandler: LinkNewDesktopHandler, - private val sessionEnterpriseService: SessionEnterpriseService, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -110,11 +107,6 @@ class LinkNewDeviceFlowNode( val data: String, ) : NavTarget - @Parcelize - data class CodeConfirmation( - val code: String, - ) : NavTarget - @Parcelize data object MobileEnterNumber : NavTarget @@ -144,14 +136,8 @@ class LinkNewDeviceFlowNode( navigateToError(linkMobileStep.errorType) } is LinkMobileStep.QrReady -> { - // 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 + // The QrCode is ready, navigate to its display + backstack.push(NavTarget.MobileShowQrCode(linkMobileStep.data)) } is LinkMobileStep.QrScanned -> { backstack.replace(NavTarget.MobileEnterNumber) @@ -159,7 +145,10 @@ class LinkNewDeviceFlowNode( LinkMobileStep.Starting -> { // This step is not received at the moment, so do nothing } - LinkMobileStep.SyncingSecrets -> Unit + LinkMobileStep.SyncingSecrets -> { + // LinkMobileStep.Done is not received at the moment, so consider that the flow is done here + callback.onDone() + } is LinkMobileStep.WaitingForAuth -> { navigateToBrowser(linkMobileStep.verificationUri) } @@ -177,9 +166,7 @@ class LinkNewDeviceFlowNode( is LinkDesktopStep.Error -> { navigateToError(linkDesktopStep.errorType) } - is LinkDesktopStep.EstablishingSecureChannel -> { - backstack.push(NavTarget.CodeConfirmation(linkDesktopStep.checkCodeString)) - } + is LinkDesktopStep.EstablishingSecureChannel -> Unit is LinkDesktopStep.InvalidQrCode -> { // This error will be handled by the ScanQrCodeNode } @@ -196,20 +183,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.InvalidCheckCode -> ErrorScreenType.Mismatch2Digits - is ErrorType.UnsupportedProtocol -> ErrorScreenType.ProtocolNotSupported - is ErrorType.Cancelled -> ErrorScreenType.Cancelled - is ErrorType.ConnectionInsecure -> ErrorScreenType.InsecureChannelDetected - 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.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.ConnectionInsecure -> ErrorScreenType.InsecureChannelDetected + is ErrorType.Expired -> ErrorScreenType.Expired + is ErrorType.OtherDeviceAlreadySignedIn -> ErrorScreenType.UnknownError + is ErrorType.UnsupportedQrCodeType -> 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. @@ -263,18 +250,6 @@ 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() { @@ -306,12 +281,8 @@ class LinkNewDeviceFlowNode( } } - private suspend fun navigateToBrowser(url: String) { - activity?.openUrlInChromeCustomTab( - session = null, - darkTheme = darkTheme, - url = sessionEnterpriseService.tweakMasUrl(url), - ) + private fun navigateToBrowser(url: String) { + activity?.openUrlInChromeCustomTab(null, darkTheme, url) } @Composable 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 18d67f577a..157d946eaa 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,7 +12,6 @@ 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 @@ -66,15 +65,4 @@ class LinkNewMobileHandler( linkMobileStepFlow.emit(LinkMobileStep.Uninitialized) } } - - fun rotateQrCode() { - createAndStartNewHandler() - } - - 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/CodeConfirmationView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationView.kt deleted file mode 100644 index d981574f86..0000000000 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationView.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 ad8cc276c5..b92a19ef8a 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,9 +20,6 @@ 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 5946eb9ab2..7fd699101b 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,6 +19,5 @@ 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 4db2aa9ad5..9f67e8bc17 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,26 +47,17 @@ 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 = iconStyle, + iconStyle = BigIcon.Style.AlertSolid, title = titleText(errorScreenType, appName), subTitle = subtitleText(errorScreenType, appName), content = { Content(errorScreenType) }, buttons = { - when (errorScreenType) { - ErrorScreenType.OtherDeviceAlreadySignedIn -> DoneButton( - onDone = onCancel, - ) - else -> Buttons( - onRetry = onRetry, - onCancel = onCancel, - ) - } + Buttons( + onRetry = onRetry, + onCancel = onCancel, + ) }, ) } @@ -81,7 +72,6 @@ private fun titleText(errorScreenType: ErrorScreenType, appName: String) = when 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 @@ -94,7 +84,6 @@ private fun subtitleText(errorScreenType: ErrorScreenType, appName: String) = wh 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 @@ -135,17 +124,6 @@ private fun Content(errorScreenType: ErrorScreenType) { } } -@Composable -private fun DoneButton( - onDone: () -> Unit, -) { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(CommonStrings.action_done), - onClick = onDone, - ) -} - @Composable private fun Buttons( onRetry: () -> 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 20bd50f488..a884c3e97f 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,7 +25,6 @@ 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, @@ -37,15 +36,11 @@ 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( - state = state, + data = inputs.data, 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 deleted file mode 100644 index 21071a6831..0000000000 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodePresenter.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 deleted file mode 100644 index e69dde8264..0000000000 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeState.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 deleted file mode 100644 index e6d33c2544..0000000000 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeStateProvider.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 f2cd07f4a5..501415f621 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,12 +9,6 @@ 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 @@ -27,7 +21,6 @@ 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 @@ -37,7 +30,6 @@ 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 @@ -46,10 +38,9 @@ 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( - state: ShowQrCodeState, + data: String, onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -64,17 +55,11 @@ fun ShowQrCodeView( Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { - AnimatedContent( - modifier = Modifier.size(220.dp), - targetState = state.data.dataOrNull(), - transitionSpec = { - fadeIn().togetherWith(fadeOut()) - } - ) { data -> - QrCodeOrLoading( - data = data, - ) - } + QrCodeImage( + data = data, + modifier = Modifier + .size(220.dp) + ) Spacer(modifier = Modifier.height(32.dp)) NumberedListOrganism( modifier = Modifier.fillMaxSize(), @@ -94,33 +79,11 @@ 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( - @PreviewParameter(ShowQrCodeStateProvider::class) state: ShowQrCodeState, -) = ElementPreview { +internal fun ShowQrCodeViewPreview() = ElementPreview { ShowQrCodeView( - state = state, + data = "DATA", onBackClick = { }, ) } 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 378a405c95..16372fa6e4 100644 --- a/features/linknewdevice/impl/src/main/res/values-be/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-be/translations.xml @@ -17,8 +17,6 @@ "Калі вы сутыкнуліся з той жа праблемай, паспрабуйце іншую сетку Wi-Fi або скарыстайцеся мабільнымі дадзенымі замест Wi-Fi." "Калі гэта не дапамагло, увайдзіце ўручную" "Злучэнне небяспечнае" - "Вам будзе прапанавана ўвесці дзве лічбы, паказаныя на гэтай прыладзе." - "Увядзіце наступны нумар на іншай прыладзе." "Уваход быў адменены на іншай прыладзе." "Запыт на ўваход скасаваны" "Уваход на іншай прыладзе быў адхілены." @@ -37,5 +35,4 @@ "Каб працягнуць, вам неабходна дазволіць %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 deleted file mode 100644 index d78744ff85..0000000000 --- a/features/linknewdevice/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - "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 e0150668a3..4b8f230d55 100644 --- a/features/linknewdevice/impl/src/main/res/values-cs/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-cs/translations.xml @@ -34,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 6b1cb7781f..b26aed52ef 100644 --- a/features/linknewdevice/impl/src/main/res/values-cy/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-cy/translations.xml @@ -17,8 +17,6 @@ "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." @@ -37,5 +35,4 @@ 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 45f510e90b..5bc9f04fb9 100644 --- a/features/linknewdevice/impl/src/main/res/values-da/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-da/translations.xml @@ -34,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 773878c736..b8ad8b80ef 100644 --- a/features/linknewdevice/impl/src/main/res/values-de/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-de/translations.xml @@ -34,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 6c0e77da40..26c917075b 100644 --- a/features/linknewdevice/impl/src/main/res/values-el/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-el/translations.xml @@ -34,8 +34,6 @@ "Εάν αντιμετωπίσεις το ίδιο πρόβλημα, δοκίμασε ένα διαφορετικό δίκτυο wifi ή χρησιμοποίησε τα δεδομένα του κινητού σου αντί για wifi" "Εάν δεν λειτουργήσει, συνδέσου χειροκίνητα" "Η σύνδεση δεν είναι ασφαλής" - "Θα σου ζητηθεί να εισάγεις τα δύο ψηφία που εμφανίζονται σε αυτήν τη συσκευή." - "Εισήγαγε τον παρακάτω αριθμό στην άλλη συσκευή σου" "Η σύνδεση ακυρώθηκε στην άλλη συσκευή." "Το αίτημα σύνδεσης ακυρώθηκε" "Η σύνδεση απορρίφθηκε στην άλλη συσκευή." @@ -56,5 +54,4 @@ "Πρέπει να δώσεις άδεια για %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 c33dc3ad88..032813a2c4 100644 --- a/features/linknewdevice/impl/src/main/res/values-es/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-es/translations.xml @@ -17,8 +17,6 @@ "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." @@ -37,5 +35,4 @@ 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 10f8af5e11..6aa1398e0a 100644 --- a/features/linknewdevice/impl/src/main/res/values-et/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-et/translations.xml @@ -34,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 8680ad94c7..06cc0fd857 100644 --- a/features/linknewdevice/impl/src/main/res/values-eu/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-eu/translations.xml @@ -16,8 +16,6 @@ "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." @@ -35,5 +33,4 @@ 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 07c329ef6a..804fa653ad 100644 --- a/features/linknewdevice/impl/src/main/res/values-fa/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-fa/translations.xml @@ -15,8 +15,6 @@ "اکنون چه؟" "ورود دستی در صورت کار نکردنش" "اتّصال ناامن" - "از شما خواسته خواهد شد که دو رقم نشان داده روی این افزاره را وارد کنید." - "شمارهٔ زیر را روی افزارهٔ دیگرتان وارد کنید" "ورود روی افزارهٔ دیگر لغو شد." "درخواست ورد لغو شد" "ورود به دست افزارهٔ دیگر رد شد." @@ -35,5 +33,4 @@ "برای ادامه باید اجازهٔ استفادهٔ %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 0ba5a30e58..f8e999f886 100644 --- a/features/linknewdevice/impl/src/main/res/values-fi/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-fi/translations.xml @@ -34,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 12a770af17..0c91dca7a1 100644 --- a/features/linknewdevice/impl/src/main/res/values-fr/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-fr/translations.xml @@ -34,8 +34,6 @@ "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." @@ -54,5 +52,4 @@ "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 8561e63ad3..20c194ef93 100644 --- a/features/linknewdevice/impl/src/main/res/values-hr/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-hr/translations.xml @@ -34,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 8cefed94cc..51fe30bbd8 100644 --- a/features/linknewdevice/impl/src/main/res/values-hu/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-hu/translations.xml @@ -34,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 5508993090..20badba9ba 100644 --- a/features/linknewdevice/impl/src/main/res/values-in/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-in/translations.xml @@ -17,8 +17,6 @@ "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." @@ -37,5 +35,4 @@ 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 6a32c70fe4..9a6476823a 100644 --- a/features/linknewdevice/impl/src/main/res/values-it/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-it/translations.xml @@ -34,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 c4f09dcd0e..6cfd5baf84 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,8 +34,6 @@ "同様の問題が発生する場合は、異なるWi-Fiやモバイルデータ通信を試してください" "問題が解決しない場合は、手動でサインインしてください" "接続が安全ではありません" - "この端末に表示される2つの数字の入力を要求されます" - "もう一方に表示される数字を入力してください" "もう一方の端末がサインインをキャンセルしました" "サインインのリクエストがキャンセルされました" "もう一方の端末でサインインを拒否されました" @@ -56,5 +54,4 @@ "続行するには、%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 1d69f58ca5..3b31c8fdc2 100644 --- a/features/linknewdevice/impl/src/main/res/values-ko/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-ko/translations.xml @@ -34,8 +34,6 @@ "동일한 문제를 겪으신 경우 다른 Wi-Fi 네트워크를 사용해 보거나 Wi-Fi 대신 모바일 데이터를 사용해 보세요." "만약 작동하지 않는 경우, 수동으로 로그인하세요." "연결이 안전하지 않습니다" - "이 장치에 표시된 두 자리 숫자를 입력하라는 메시지가 표시됩니다." - "다른 device 에 아래 번호를 입력하세요" "다른 기기에서 로그인이 취소되었습니다." "로그인 요청이 취소되었습니다" "다른 기기에서 로그인이 거부되었습니다." @@ -56,5 +54,4 @@ "계속하려면 %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 4e8844ee2f..6b8541b25b 100644 --- a/features/linknewdevice/impl/src/main/res/values-nb/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-nb/translations.xml @@ -34,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 6dbc2c18c2..407a470e48 100644 --- a/features/linknewdevice/impl/src/main/res/values-nl/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-nl/translations.xml @@ -17,8 +17,6 @@ "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." @@ -37,5 +35,4 @@ 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 ced8955d1d..4db42a2a49 100644 --- a/features/linknewdevice/impl/src/main/res/values-pl/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-pl/translations.xml @@ -1,47 +1,26 @@ "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. @@ -56,5 +35,4 @@ 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 1680e5c0ff..f11bdc6e6d 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,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 eff27b17f0..da6da08f38 100644 --- a/features/linknewdevice/impl/src/main/res/values-pt/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-pt/translations.xml @@ -17,8 +17,6 @@ "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." @@ -37,5 +35,4 @@ 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 57d439240d..f1a4f3db59 100644 --- a/features/linknewdevice/impl/src/main/res/values-ro/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-ro/translations.xml @@ -26,7 +26,6 @@ "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?" @@ -34,14 +33,10 @@ "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. @@ -56,5 +51,4 @@ "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 6a8b645c4f..39506417b6 100644 --- a/features/linknewdevice/impl/src/main/res/values-ru/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-ru/translations.xml @@ -34,8 +34,6 @@ "Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные" "Если это не помогло, войдите вручную" "Соединение не защищено" - "Вам нужно будет ввести две цифры, показанные на этом устройстве." - "Введите показанный номер на своем другом устройстве" "Вход на другом устройстве был отменен." "Запрос на вход отменен" "Вход в систему был отклонен на другом устройстве." @@ -56,5 +54,4 @@ "Чтобы продолжить, вам необходимо разрешить %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 f64c01328b..cb430671bb 100644 --- a/features/linknewdevice/impl/src/main/res/values-sk/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-sk/translations.xml @@ -34,8 +34,6 @@ "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í." @@ -56,5 +54,4 @@ 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 8800b31bbd..8a1bef434a 100644 --- a/features/linknewdevice/impl/src/main/res/values-sv/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-sv/translations.xml @@ -17,8 +17,6 @@ "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." @@ -37,5 +35,4 @@ 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 3c8767f64c..e5fd989c0b 100644 --- a/features/linknewdevice/impl/src/main/res/values-tr/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-tr/translations.xml @@ -33,8 +33,6 @@ "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." @@ -53,5 +51,4 @@ 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 752b5ead3f..875b5aed16 100644 --- a/features/linknewdevice/impl/src/main/res/values-uk/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-uk/translations.xml @@ -26,7 +26,6 @@ "Завантаження QR-коду…" "Мобільний пристрій" "Який тип пристрою ви хочете під\'єднати?" - "Спробуйте ще раз і переконайтеся, що ви правильно ввели двозначний код. Якщо цифри все одно не збігаються, зверніться до свого провайдера облікового запису." "Цифри не збігаються" "Не вдалося встановити безпечне з\'єднання з новим пристроєм. Ваші наявні пристрої досі в безпеці, і вам не потрібно про них турбуватися." "Що тепер?" @@ -34,8 +33,6 @@ "Якщо ви зіткнулися з тією ж проблемою, спробуйте іншу мережу Wi-Fi або використовуйте мобільний інтернет замість Wi-Fi" "Якщо це не спрацює, увійдіть вручну" "З\'єднання не безпечне" - "Вас попросять ввести дві цифри, показані на цьому пристрої." - "Введіть номер нижче на іншому пристрої" "Вхід було скасовано на іншому пристрої." "Запит на вхід скасовано" "Вхід був відхилений на іншому пристрої." @@ -56,5 +53,4 @@ "Вам потрібно дати дозвіл %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 0b4bb02226..54d2c2e401 100644 --- a/features/linknewdevice/impl/src/main/res/values-ur/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-ur/translations.xml @@ -17,8 +17,6 @@ "اگر آپ کو بھی یہی مسئلہ درپیش ہو، تو کوئی دوسرا وائی فائی شبکہ آزمائیں یا وائی فائی کے بجائے اپنے محمول بیانات استعمال کریں۔" "اگر یہ کام نہ کرے، تو دستی طور پر داخل ہوں" "اتصال محفوظ نہیں" - "آپ سے اس آلے پر دکھائے گئے دو ہندسوں کو درج کرنے کو کہا جائے گا۔" - "اپنے دوسرے آلے پر درج ذیل نمبر درج کریں" "دوسرے آلے پر دخول منسوخ کر دیا گیا تھا۔" "دخول کی درخواست منسوخ" "دوسرے آلہ پر دخول کو مسترد کر دیا گیا تھا۔" @@ -37,5 +35,4 @@ "جاری رکھنے کے لیے آپ %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 1d436b5f4a..b1f3deebf4 100644 --- a/features/linknewdevice/impl/src/main/res/values-uz/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-uz/translations.xml @@ -26,7 +26,6 @@ "QR kod yuklanmoqda…" "Mobil qurilma" "Qaysi turdagi qurilmani bog‘lashni xohlaysiz?" - "Qayta urining va 2 xonali kodni bexato kiritganingizni tekshiring. Agar raqamlar hali ham mos kelmasa, hisobingiz provayderiga murojaat qiling." "Raqamlar mos kelmaydi" "Yangi qurilmaga xavfsiz ulanish amalga oshirilmadi. Mavjud qurilmalaringiz hali ham xavfsiz va ular haqida qaygʻurishingiz shart emas." "Endi nima?" @@ -34,14 +33,10 @@ "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." "Tizimga kirish rad etildi" - "Boshqa hech narsa qilishingiz shart emas." - "Boshqa qurilmangiz allaqachon tizimga kirgan" "Kirish muddati tugagan. Iltimos, qayta urinib koʻring." "Kirish oʻz vaqtida tugallanmagan" "Boshqa qurilmangiz %s hisobiga QR kod orqali kirishni qoʻllab-quvvatlamaydi. @@ -56,5 +51,4 @@ 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 e18fbc13b6..9b3cd5ead5 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 @@ -1,47 +1,26 @@ "掃描 QR code" - "在筆記型電腦或桌上型電腦上開啟 %1$s" "使用此裝置掃描 QR code" "準備掃描" - "在桌上型電腦上開啟 %1$s 以取得 QR code" - "數字不符" - "輸入兩位數代碼" - "這將確認您與另一台裝置之間的連線是否安全。" - "輸入顯示在您的其他裝置上的數字" "您的帳號提供者不支援 %1$s。" "不支援 %1$s" - "您的帳號提供者不支援使用 QR code 登入新裝置。" "不支援 QR code" "已在其他裝置上取消登入。" "已取消登入請求" "登入已過期。請再試一次。" "未及時完成登入" - "在其他裝置上開啟 %1$s" "選取 %1$s" - "「使用 QR code 登入」" - "使用其他裝置掃描此處顯示的 QR code" - "在其他裝置上開啟 %1$s" - "桌上型電腦" - "正在載入 QR code……" - "行動裝置" - "您想連結哪種類型的裝置?" - "請重試,並確定您已輸入兩位數代碼。若數字仍然不符,請聯絡您的帳號提供者。" - "數字不符" "無法與新裝置建立安全連線。您現有的裝置仍然安全,您不必擔心它們。" "現在怎麼辦?" "嘗試再次使用 QR code 登入以確認不是網路問題" "如果遇到相同的問題,請嘗試使用其他 wifi 網路或您的行動數據" "若無法運作,請手動登入" "連線不安全" - "系統會要求您輸入此裝置上顯示的兩位數字。" - "在您的其他裝置上輸入以下數字" "已在其他裝置上取消登入。" "已取消登入請求" "其他裝置拒絕登入。" "已拒絕登入" - "您不需要進行其他操作。" - "您的其他裝置已登入" "登入已過期。請再試一次。" "未及時完成登入" "您的其他裝置不支援使用 QR cpde 登入 %s。 @@ -56,5 +35,4 @@ "您必須授予 %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 f11f60dd61..10ae1ae7a7 100644 --- a/features/linknewdevice/impl/src/main/res/values-zh/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-zh/translations.xml @@ -1,18 +1,18 @@ "扫描二维码" - "在笔记本电脑或台式机上打开 %1$s" + "在笔记本电脑或台式机上打开%1$s " "使用此设备扫描二维码" "准备进行扫描" - "在台式电脑上打开 %1$s 以获取二维码" + "在电脑上打开%1$s 获取二维码" "数字不匹配" - "输入两位数字的代码" - "这将验证你与其它设备的连接是否安全。" + "输入两位数的验证码" + "这将验证您与其他设备的连接是否安全。" "请输入另一台设备上显示的数字" - "账户提供者不支持 %1$s." + "账户提供方不支持 %1$s." "不支持 %1$s." - "你的账户提供者不支持使用二维码登录到新设备。" - "二维码不受支持" + "您的账户提供商不支持使用二维码登录新设备。" + "不支持二维码" "登录被另一台设备取消" "登录请求已取消" "登录已过期. 请重试." @@ -25,36 +25,33 @@ "台式计算机" "正在加载二维码…" "移动设备" - "你想连接哪种类型的设备?" - "请重试,并确保已正确输入两位数字的代码。如果数字仍然不匹配,请联系账户提供者。" + "您想连接哪种类型的设备?" + "请重试,并确保您已正确输入两位验证码。如果验证码仍然不匹配,请联系您的账户提供商。" "数字不匹配" - "无法与新设备建立安全连接。你的现有设备仍然安全,无需担心。" + "无法与新设备建立安全连接。您现有的设备仍然安全,无需担心。" "现在怎么办?" "如果这是网络问题,请尝试使用二维码再次登录" "如果遇到同样的问题,请尝试使用不同的 WiFi 网络或使用移动数据代替 WiFi" "如果不起作用,请手动登录" "连接不安全" - "你将被要求输入此设备上显示的两位数字。" - "在你的其它设备上输入以下数字" "登录被另一台设备取消" "登录请求已取消" - "另一设备上的登录请求已被拒绝。" + "其它设备未接受请求" "登录被拒绝" - "无需额外操作。" - "你已在另一设备上登录。" + "您无需额外操作。" + "您已在另一台设备登录。" "登录已过期. 请重试." "登录未及时完成" "另一个设备不支持使用二维码登录 %s. 尝试手动或使用另一个设备扫描二维码." - "二维码不受支持" - "账户提供者不支持 %1$s." + "不支持二维码" + "账户提供方不支持 %1$s." "不支持 %1$s." - "使用其它设备上显示的二维码。" - "重试" + "使用其他设备上显示的二维码。" + "再试一次" "二维码错误" - "你需要授予 %1$s 使用设备摄像头的权限才能继续。" - "允许访问摄像头以扫描二维码" + "您需要授予 %1$s 使用设备摄像头的权限才能继续。" + "允许摄像头权限以扫描 QR 码" "发生了意外错误。请再试一次。" - "正在等待其它设备" diff --git a/features/linknewdevice/impl/src/main/res/values/localazy.xml b/features/linknewdevice/impl/src/main/res/values/localazy.xml index 6ffcce227a..321b168751 100644 --- a/features/linknewdevice/impl/src/main/res/values/localazy.xml +++ b/features/linknewdevice/impl/src/main/res/values/localazy.xml @@ -34,8 +34,6 @@ "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." @@ -56,5 +54,4 @@ 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 1b2af8f4c3..2957a89495 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,6 @@ 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.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.lambda.lambdaError @@ -38,7 +37,6 @@ class DefaultLinkNewDeviceEntryPointTest { sessionCoroutineScope = backgroundScope, linkNewMobileHandler = LinkNewMobileHandler(client), linkNewDesktopHandler = LinkNewDesktopHandler(client), - sessionEnterpriseService = FakeSessionEnterpriseService(), ) } 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 7609acf809..ac0a129f49 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,14 +5,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.linknewdevice.impl.R import io.element.android.tests.testutils.EnsureNeverCalled @@ -21,37 +18,42 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class DesktopNoticeViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { + fun `on back pressed - calls the expected callback`() { ensureCalledOnce { callback -> - setView( + rule.setView( state = aDesktopNoticeState(), onBackClicked = callback, ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest { + fun `on back button clicked - calls the expected callback`() { ensureCalledOnce { callback -> - setView( + rule.setView( state = aDesktopNoticeState(), onBackClicked = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `when can continue - calls the expected callback`() = runAndroidComposeUiTest { + fun `when can continue - calls the expected callback`() { ensureCalledOnce { callback -> - setView( + rule.setView( state = aDesktopNoticeState(canContinue = true), onReadyToScanClick = callback, ) @@ -59,16 +61,16 @@ class DesktopNoticeViewTest { } @Test - fun `on submit button clicked - emits the Continue event`() = runAndroidComposeUiTest { + fun `on submit button clicked - emits the Continue event`() { val eventRecorder = EventsRecorder() - setView( + rule.setView( state = aDesktopNoticeState(eventSink = eventRecorder), ) - clickOn(R.string.screen_link_new_device_desktop_submit) + rule.clickOn(R.string.screen_link_new_device_desktop_submit) eventRecorder.assertSingle(DesktopNoticeEvent.Continue) } - private fun AndroidComposeUiTest.setView( + private fun AndroidComposeTestRule.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 b63d7471ac..aa52a70149 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,56 +5,58 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ErrorViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `on back pressed - calls the onCancel callback`() = runAndroidComposeUiTest { + fun `on back pressed - calls the onCancel callback`() { ensureCalledOnce { callback -> - setErrorView( + rule.setErrorView( onCancel = callback, ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `on try again button clicked - calls the expected callback`() = runAndroidComposeUiTest { + fun `on try again button clicked - calls the expected callback`() { ensureCalledOnce { callback -> - setErrorView( + rule.setErrorView( onRetry = callback ) - clickOn(CommonStrings.action_try_again) + rule.clickOn(CommonStrings.action_try_again) } } @Test - fun `on cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest { + fun `on cancel button clicked - calls the expected callback`() { ensureCalledOnce { callback -> - setErrorView( + rule.setErrorView( onCancel = callback ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) } } - private fun AndroidComposeUiTest.setErrorView( + private fun AndroidComposeTestRule.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/EnterNumberViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberViewTest.kt index 25dc9efa8a..20e1d898dd 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,16 +5,13 @@ * 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -23,60 +20,65 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EnterNumberViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { + fun `on back pressed - calls the expected callback`() { ensureCalledOnce { callback -> - setView( + rule.setView( state = aEnterNumberState(), onBackClicked = callback, ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest { + fun `on back button clicked - calls the expected callback`() { ensureCalledOnce { callback -> - setView( + rule.setView( state = aEnterNumberState(), onBackClicked = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `on continue button clicked - emits the Continue event`() = runAndroidComposeUiTest { + fun `on continue button clicked - emits the Continue event`() { val eventRecorder = EventsRecorder() - setView( + rule.setView( state = aEnterNumberState( number = "12", eventSink = eventRecorder, ), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventRecorder.assertSingle(EnterNumberEvent.Continue) } @Test - fun `when the number is not complete, continue button is disabled`() = runAndroidComposeUiTest { + fun `when the number is not complete, continue button is disabled`() { val eventRecorder = EventsRecorder(expectEvents = false) - setView( + rule.setView( state = aEnterNumberState( number = "1", eventSink = eventRecorder, ), ) - val continueStr = activity!!.getString(CommonStrings.action_continue) - onNodeWithText(continueStr).assertIsNotEnabled() + val continueStr = rule.activity.getString(CommonStrings.action_continue) + rule.onNodeWithText(continueStr).assertIsNotEnabled() } - private fun AndroidComposeUiTest.setView( + private fun AndroidComposeTestRule.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 deleted file mode 100644 index f92cf66102..0000000000 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodePresenterTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package io.element.android.features.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 7927eeed77..c6c89ba818 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,39 +5,41 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShowQrCodeViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { + fun `on back pressed - calls the expected callback`() { ensureCalledOnce { callback -> - setView( + rule.setView( onBackClick = callback ) - pressBackKey() + rule.pressBackKey() } } - private fun AndroidComposeUiTest.setView( + private fun AndroidComposeTestRule.setView( onBackClick: () -> Unit = EnsureNeverCalled(), ) { setContent { ShowQrCodeView( - state = aShowQrCodeState(), + data = "DATA", onBackClick = onBackClick, ) } 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 bceb8753b2..e352debfb0 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,14 +5,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.linknewdevice.impl.R import io.element.android.libraries.architecture.AsyncData @@ -22,69 +19,74 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LinkNewDeviceRootViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `on back pressed - calls the onRetry callback`() = runAndroidComposeUiTest { + fun `on back pressed - calls the onRetry callback`() { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setLinkNewDeviceRootView( + rule.setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( eventSink = eventRecorder, ), onBackClick = callback ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `link desktop button clicked - calls the expected callback`() = runAndroidComposeUiTest { + fun `link desktop button clicked - calls the expected callback`() { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setLinkNewDeviceRootView( + rule.setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( isSupported = AsyncData.Success(true), eventSink = eventRecorder, ), onLinkDesktopDeviceClick = callback, ) - clickOn(R.string.screen_link_new_device_root_desktop_computer) + rule.clickOn(R.string.screen_link_new_device_root_desktop_computer) } } @Test - fun `link mobile button clicked - emits the expected event`() = runAndroidComposeUiTest { + fun `link mobile button clicked - emits the expected event`() { val eventRecorder = EventsRecorder() - setLinkNewDeviceRootView( + rule.setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( isSupported = AsyncData.Success(true), eventSink = eventRecorder, ) ) - clickOn(R.string.screen_link_new_device_root_mobile_device) + rule.clickOn(R.string.screen_link_new_device_root_mobile_device) eventRecorder.assertSingle(LinkNewDeviceRootEvent.LinkMobileDevice) } @Test - fun `not supported - dismiss click - invokes the expected callback`() = runAndroidComposeUiTest { + fun `not supported - dismiss click - invokes the expected callback`() { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setLinkNewDeviceRootView( + rule.setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( isSupported = AsyncData.Success(false), eventSink = eventRecorder, ), onBackClick = callback, ) - clickOn(CommonStrings.action_dismiss) + rule.clickOn(CommonStrings.action_dismiss) } } - private fun AndroidComposeUiTest.setLinkNewDeviceRootView( + private fun AndroidComposeTestRule.setLinkNewDeviceRootView( state: LinkNewDeviceRootState = aLinkNewDeviceRootState(), onBackClick: () -> Unit = EnsureNeverCalled(), onLinkDesktopDeviceClick: () -> Unit = EnsureNeverCalled(), 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 1932718fef..fcc3afeb7d 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,14 +5,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -22,39 +19,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.pressBackKey +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ScanQrCodeViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { + fun `on back pressed - calls the expected callback`() { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setView( + rule.setView( state = aScanQrCodeState( eventSink = eventRecorder, ), onBackClick = callback ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `try again button clicked - emits the expected event`() = runAndroidComposeUiTest { + fun `try again button clicked - emits the expected event`() { val eventRecorder = EventsRecorder() - setView( + rule.setView( state = aScanQrCodeState( scanAction = AsyncAction.Failure(AN_EXCEPTION), eventSink = eventRecorder, ) ) - clickOn(CommonStrings.action_try_again) + rule.clickOn(CommonStrings.action_try_again) eventRecorder.assertSingle(ScanQrCodeEvent.TryAgain) } - private fun AndroidComposeUiTest.setView( + private fun AndroidComposeTestRule.setView( state: ScanQrCodeState = aScanQrCodeState(), onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts index f8377389f1..ab85e37594 100644 --- a/features/location/api/build.gradle.kts +++ b/features/location/api/build.gradle.kts @@ -71,7 +71,6 @@ 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 deleted file mode 100644 index 8c53550737..0000000000 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/LiveLocationSharingBanner.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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/ShowLocationMode.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/ShowLocationMode.kt index 3feeeff57d..1227ddec46 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/ShowLocationMode.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/ShowLocationMode.kt @@ -24,7 +24,5 @@ sealed interface ShowLocationMode : Parcelable { ) : ShowLocationMode @Parcelize - data class Live( - val senderId: UserId - ) : ShowLocationMode + data object Live : ShowLocationMode } 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..0657bae634 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 @@ -9,9 +9,7 @@ package io.element.android.features.location.api import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -24,8 +22,6 @@ 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.res.painterResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil3.Extras import coil3.compose.AsyncImagePainter @@ -42,16 +38,11 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight /** * Shows a static map image downloaded via a third party service's static maps API. - * - * Handles 4 distinct cases: - * 1. Stale location (pinVariant is StaleLocation) - shows stale map with stale pin, no fetching - * 2. Null location - shows blurred placeholder, no pin, no loading - * 3. Loading (location != null, fetching) - shows blurred placeholder with loading indicator - * 4. Success (location != null, loaded) - shows actual map with pin */ @Composable fun StaticMapView( - location: Location?, + lat: Double, + lon: Double, zoom: Double, pinVariant: PinVariant, contentDescription: String?, @@ -65,111 +56,50 @@ fun StaticMapView( modifier = modifier, contentAlignment = Alignment.Center ) { - // Case 1: Stale location - show stale map with stale pin, no fetching - when { - pinVariant is PinVariant.StaleLocation -> { - StaleMapContent( - pinVariant = pinVariant, - contentDescription = contentDescription, - width = maxWidth, - height = maxHeight, - ) - } - // Case 2: Null location - show blurred placeholder, no pin, no loading - location == null -> { - StaticMapPlaceholder( - painter = painterResource(R.drawable.blurred_map), - canReload = false, - contentDescription = contentDescription, - width = maxWidth, - height = maxHeight, - onLoadMapClick = {} - ) - } - // Cases 3 & 4: Non-null location - fetch map - else -> LoadableMapContent( - location = location, - zoom = zoom, - pinVariant = pinVariant, - contentDescription = contentDescription, - darkMode = darkMode, - ) - } - } -} - -@Composable -private fun BoxWithConstraintsScope.StaleMapContent( - pinVariant: PinVariant, - contentDescription: String?, - width: Dp, - height: Dp, -) { - Box(contentAlignment = Alignment.Center) { - Image( - painter = painterResource(R.drawable.stale_map), - contentDescription = contentDescription, - contentScale = ContentScale.FillBounds, - modifier = Modifier.size(width = width, height = height) - ) - LocationPin(variant = pinVariant, modifier = Modifier.centerBottomEdge(this@StaleMapContent)) - } -} - -@Composable -private fun BoxWithConstraintsScope.LoadableMapContent( - location: Location, - zoom: Double, - pinVariant: PinVariant, - contentDescription: String?, - darkMode: Boolean, -) { - val context = LocalContext.current - 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 context = LocalContext.current + 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 (else it will thrown an exception). + null + } else { + ImageRequest.Builder(context) + .data( + builder.build( + lat = lat, + lon = 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() - when (state) { - is AsyncImagePainter.State.Success -> { + val collectedState = painter.state.collectAsState() + if (collectedState.value is AsyncImagePainter.State.Success) { Image( painter = painter, contentDescription = contentDescription, 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. + // a max width and height of 2048 px. See buildStaticMapsApiUrl() for more details. + // We apply ContentScale.Fit to scale the image to fill the AsyncImage should this be the case. contentScale = ContentScale.Fit, ) LocationPin(variant = pinVariant, modifier = Modifier.centerBottomEdge(this)) - } - else -> { + } else { StaticMapPlaceholder( - painter = painterResource(R.drawable.blurred_map), - canReload = builder.isServiceAvailable() && state is AsyncImagePainter.State.Error, + showProgress = collectedState.value.isLoading(), + canReload = builder.isServiceAvailable(), contentDescription = contentDescription, width = maxWidth, height = maxHeight, @@ -179,11 +109,17 @@ private fun BoxWithConstraintsScope.LoadableMapContent( } } +private fun AsyncImagePainter.State.isLoading(): Boolean { + return this is AsyncImagePainter.State.Empty || + this is AsyncImagePainter.State.Loading +} + @PreviewsDayNight @Composable internal fun StaticMapViewPreview() = ElementPreview { StaticMapView( - location = Location(0.0, 0.0), + lat = 0.0, + lon = 0.0, zoom = 0.0, contentDescription = null, pinVariant = PinVariant.PinnedLocation, diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt index 0292ec927e..81b80c8dc3 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt @@ -18,7 +18,6 @@ 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.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -28,13 +27,14 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.location.api.R 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.Text import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun StaticMapPlaceholder( - painter: Painter, + showProgress: Boolean, canReload: Boolean, contentDescription: String?, width: Dp, @@ -46,15 +46,17 @@ internal fun StaticMapPlaceholder( contentAlignment = Alignment.Center, modifier = modifier .size(width = width, height = height) - .clickable(enabled = canReload, onClick = onLoadMapClick) + .then(if (showProgress) Modifier else Modifier.clickable(onClick = onLoadMapClick)) ) { Image( - painter = painter, + painter = painterResource(id = R.drawable.blurred_map), contentDescription = contentDescription, contentScale = ContentScale.FillBounds, modifier = Modifier.size(width = width, height = height) ) - if (canReload) { + if (showProgress) { + CircularProgressIndicator() + } else if (canReload) { Column( horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -75,10 +77,13 @@ internal fun StaticMapPlaceholderPreview() = ElementPreview { modifier = Modifier.padding(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - listOf(false, true) - .forEach { canReload -> + listOf( + true to false, + false to true, + false to false, + ).forEach { (showProgress, canReload) -> StaticMapPlaceholder( - painter = painterResource(R.drawable.blurred_map), + showProgress = showProgress, canReload = canReload, contentDescription = null, width = 400.dp, 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 deleted file mode 100644 index cd6b8731c1..0000000000 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/live/ActiveLiveLocationShareManager.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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/main/res/drawable-night/stale_map.png b/features/location/api/src/main/res/drawable-night/stale_map.png deleted file mode 100644 index 9e36759203..0000000000 Binary files a/features/location/api/src/main/res/drawable-night/stale_map.png and /dev/null differ diff --git a/features/location/api/src/main/res/drawable/stale_map.png b/features/location/api/src/main/res/drawable/stale_map.png deleted file mode 100644 index 87fa0188c9..0000000000 Binary files a/features/location/api/src/main/res/drawable/stale_map.png and /dev/null differ diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts index 165c32b7c5..0da54a1394 100644 --- a/features/location/impl/build.gradle.kts +++ b/features/location/impl/build.gradle.kts @@ -37,16 +37,10 @@ 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) @@ -56,7 +50,4 @@ 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 e92ca68077..ae728c09e1 100644 --- a/features/location/impl/src/main/AndroidManifest.xml +++ b/features/location/impl/src/main/AndroidManifest.xml @@ -9,14 +9,4 @@ - - - - - - - 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 f90793b775..a0b0cd4734 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 @@ -16,16 +16,13 @@ sealed interface LocationConstraintsCheck { data object PermissionRationale : 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 @@ -44,6 +41,5 @@ fun LocationConstraintsCheck.toDialogState(): LocationConstraintsDialogState { 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 deleted file mode 100644 index d1a9e32026..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/SendLiveLocationPermissions.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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/ui/LocationConstraintsDialog.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationConstraintsDialog.kt index 334aebaee6..95f5129f91 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,8 +10,6 @@ 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 @@ -44,10 +42,6 @@ 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 - ) } } @@ -57,5 +51,4 @@ 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/LocationShareRow.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationShareRow.kt index 24476e3c66..b949f55c76 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,7 +15,6 @@ 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 @@ -32,8 +31,6 @@ 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 @@ -45,7 +42,6 @@ import io.element.android.libraries.ui.strings.CommonStrings fun LocationShareRow( item: LocationShareItem, onShareClick: () -> Unit, - onStopClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -95,32 +91,19 @@ fun LocationShareRow( ) } Text( - text = if (item.isLive) stringResource(CommonStrings.screen_room_live_location_banner) else item.formattedTimestamp, + text = item.formattedTimestamp, style = ElementTheme.typography.fontBodySmRegular, - color = if (item.isLive) ElementTheme.colors.textPrimary else ElementTheme.colors.textSecondary, + color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } - 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, ) } } @@ -133,39 +116,35 @@ internal fun LocationShareRowPreview() = ElementPreview { LocationShareRow( item = LocationShareItem( userId = UserId("@alice:matrix.org"), - displayName = USER_NAME_ALICE, + displayName = "Alice", avatarData = AvatarData( id = "@alice:matrix.org", - name = USER_NAME_ALICE, + name = "Alice", url = null, size = AvatarSize.UserListItem, ), formattedTimestamp = "Shared 1 min ago", isLive = true, assetType = AssetType.SENDER, - location = Location(0.0, 0.0), - isOwnUser = true, + location = Location(0.0, 0.0) ), - onStopClick = {}, onShareClick = {}, ) LocationShareRow( item = LocationShareItem( userId = UserId("@bob:matrix.org"), - displayName = USER_NAME_BOB, + displayName = "Bob", avatarData = AvatarData( id = "@bob:matrix.org", - name = USER_NAME_BOB, + name = "Bob", url = null, size = AvatarSize.UserListItem, ), isLive = false, assetType = AssetType.PIN, formattedTimestamp = "Shared 5 hours ago", - location = Location(0.0, 0.0), - isOwnUser = false + location = Location(0.0, 0.0) ), - 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..fbaed9c854 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 @@ -10,14 +10,12 @@ package io.element.android.features.location.impl.common.ui 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.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.safeDrawing @@ -45,7 +43,6 @@ 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.core.data.tryOrNull -import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.rememberCameraState @@ -115,11 +112,8 @@ fun MapBottomSheetScaffold( modifier = Modifier, sheetPeekHeight = sheetPeekHeight, sheetContent = { - val maxContentHeight = (layoutHeightPx * 0.5f).roundToInt().toDp() - Column(modifier = Modifier.heightIn(max = maxContentHeight)) { - sheetContent(sheetPadding) - Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) - } + sheetContent(sheetPadding) + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) }, scaffoldState = scaffoldState, sheetDragHandle = sheetDragHandle, 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 589ed87c6f..8b89f77be4 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 @@ -23,7 +23,7 @@ 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.seconds +import kotlin.time.Duration.Companion.minutes @Composable fun UserLocationPuck( @@ -72,9 +72,9 @@ fun rememberUserLocationState(hasLocationPermission: Boolean): UserLocationState rememberNullLocationProvider() } else { rememberAndroidLocationProvider( - updateInterval = 5.seconds, - desiredAccuracy = DesiredAccuracy.High, - minDistanceMeters = 5f, + 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/live/DefaultActiveLiveLocationShareManager.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/DefaultActiveLiveLocationShareManager.kt deleted file mode 100644 index fd16bea515..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/DefaultActiveLiveLocationShareManager.kt +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 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 deleted file mode 100644 index 417d9d423a..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/LiveLocationStore.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 deleted file mode 100644 index 9d4c461b0e..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/notification/LiveLocationSharingNotificationCreator.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 deleted file mode 100644 index adba75730c..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationReceiver.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.location.impl.live.service - -import io.element.android.features.location.api.Location - -fun interface LiveLocationReceiver { - suspend fun onLocationUpdate(location: Location) -} 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 deleted file mode 100644 index e39acb14e8..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingCoordinator.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 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 deleted file mode 100644 index 4451febb19..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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.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.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import org.maplibre.compose.location.AndroidLocationProvider -import org.maplibre.compose.location.DesiredAccuracy -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") - runCatchingExceptions { - bindings().inject(this) - 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") - stopSelf() - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - private fun startLocationUpdatesListener() { - Timber.d("LiveLocationSharingService listening to location updates") - appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow() - .flatMapLatest { minDistanceMeters -> - val locationProvider = AndroidLocationProvider( - context = applicationContext, - updateInterval = UPDATE_INTERVAL_IN_SECOND.seconds, - minDistanceMeters = minDistanceMeters.toFloat(), - desiredAccuracy = DesiredAccuracy.Balanced, - coroutineScope = coroutineScope - ) - locationProvider.location - } - .filterNotNull() - .map { location -> - ApiLocation( - lat = location.position.latitude, - lon = location.position.longitude, - accuracy = location.accuracy.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 e560ce805f..d9ebc8b5af 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,8 +17,7 @@ sealed interface ShareLocationEvent { val isPinned: Boolean, ) : ShareLocationEvent - data object InitiateLiveLocationShare : ShareLocationEvent - data object AcceptLiveLocationDisclaimer : ShareLocationEvent + data object ShowLiveLocationDurationPicker : 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 a1e45cfea2..10fddf1e50 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 @@ -21,30 +21,26 @@ 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.live.LiveLocationStore +import io.element.android.features.location.impl.share.ShareLocationState.Dialog.Constraints import io.element.android.features.messages.api.MessageComposerContext -import io.element.android.libraries.architecture.AsyncAction 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 @@ -64,10 +60,9 @@ 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, ) : Presenter { @AssistedFactory fun interface Factory { @@ -80,43 +75,22 @@ 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) } - val startLiveLocationAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val currentUser by client.userProfile.collectAsState() - val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms -> - perms.sendLiveLocationPermissions() - } val scope = rememberCoroutineScope() fun checkLocationConstraints() { - // No need to check SendLiveLocationPermissions here - val locationConstraints = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) - dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState()) + val locationConstraints = checkLocationConstraints(permissionsState, locationActions) + dialogState = Constraints(locationConstraints.toDialogState()) trackUserPosition = locationConstraints is LocationConstraintsCheck.Success } - suspend fun computeLiveLocationDialogState(): ShareLocationState.Dialog { - val hasAcceptedDisclaimer = liveLocationStore.hasAcceptedLiveLocationDisclaimer() - val constraintsResult = checkLocationConstraints(permissionsState, locationActions, sendLiveLocationPermissions) - return when { - !hasAcceptedDisclaimer -> { - ShareLocationState.Dialog.LiveLocationDisclaimer - } - constraintsResult is LocationConstraintsCheck.Success -> { - val durations = LIVE_LOCATION_DURATIONS.map { - LiveLocationDuration(duration = it, formatted = durationFormatter.format(it)) - } - ShareLocationState.Dialog.LiveLocationDurations(durations.toImmutableList()) - } - else -> { - ShareLocationState.Dialog.Constraints(constraintsResult.toDialogState()) - } - } - } - LaunchedEffect(permissionsState.permissions) { checkLocationConstraints() } fun handleEvent(event: ShareLocationEvent) { @@ -135,23 +109,20 @@ class ShareLocationPresenter( locationActions.openLocationSettings() dialogState = ShareLocationState.Dialog.None } - ShareLocationEvent.InitiateLiveLocationShare -> scope.launch { - dialogState = computeLiveLocationDialogState() - } - ShareLocationEvent.AcceptLiveLocationDisclaimer -> scope.launch { - liveLocationStore.setAcceptedLiveLocationDisclaimer() - .onSuccess { - dialogState = computeLiveLocationDialogState() + 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)) } + ShareLocationState.Dialog.LiveLocationDurations(durations.toImmutableList()) + } else { + Constraints(constraintsResult.toDialogState()) + } } is ShareLocationEvent.StartLiveLocationShare -> scope.launch { dialogState = ShareLocationState.Dialog.None - startLiveLocationAction.runUpdatingState { - liveLocationShareManager.startShare( - roomId = room.roomId, - duration = event.duration, - ) - } + // room.startLiveLocationShare(event.duration.inWholeMilliseconds) } ShareLocationEvent.RequestPermissions -> { dialogState = ShareLocationState.Dialog.None @@ -165,9 +136,8 @@ class ShareLocationPresenter( dialogState = dialogState, trackUserLocation = trackUserPosition, hasLocationPermission = permissionsState.isAnyGranted, - canShareLiveLocation = timelineMode.canShareLiveLocation(), + canShareLiveLocation = isLiveLocationSharingEnabled, appName = appName, - startLiveLocationAction = startLiveLocationAction.value, eventSink = ::handleEvent, ) } @@ -204,9 +174,4 @@ 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 68598cba04..8b1f494f1e 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,7 +9,6 @@ package io.element.android.features.location.impl.share import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList @@ -20,13 +19,11 @@ data class ShareLocationState( val hasLocationPermission: Boolean, 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 ae1b765b6b..facef74346 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,7 +10,6 @@ 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.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.persistentListOf @@ -52,18 +51,6 @@ class ShareLocationStateProvider : PreviewParameterProvider trackUserPosition = true, hasLocationPermission = true, ), - aShareLocationState( - dialogState = ShareLocationState.Dialog.None, - trackUserPosition = true, - hasLocationPermission = true, - canShareLiveLocation = true, - ), - aShareLocationState( - dialogState = ShareLocationState.Dialog.LiveLocationDisclaimer, - trackUserPosition = true, - hasLocationPermission = true, - canShareLiveLocation = true, - ), aShareLocationState( dialogState = ShareLocationState.Dialog.LiveLocationDurations( persistentListOf( @@ -86,7 +73,6 @@ fun aShareLocationState( hasLocationPermission: Boolean = false, canShareLiveLocation: Boolean = false, appName: String = APP_NAME, - startLiveLocationAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (ShareLocationEvent) -> Unit = {}, ): ShareLocationState { return ShareLocationState( @@ -96,7 +82,6 @@ fun aShareLocationState( hasLocationPermission = hasLocationPermission, 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 e20ee3a7a5..1e163f417d 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 @@ -29,6 +29,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.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -43,16 +44,11 @@ import io.element.android.features.location.impl.common.ui.LocationFloatingActio 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.share.ShareLocationEvent.StartLiveLocationShare -import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.androidutils.system.toast 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 @@ -78,6 +74,7 @@ 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( @@ -88,17 +85,12 @@ 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(StartLiveLocationShare(duration)) + state.eventSink(ShareLocationEvent.StartLiveLocationShare(duration)) + context.toast("Not implemented yet!") + navigateUp() }, onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) }, ) @@ -168,46 +160,10 @@ fun ShareLocationView( .align(Alignment.TopEnd) .padding(all = 16.dp), ) - StartLiveLocationActionView(state.startLiveLocationAction, navigateUp) } ) } -@Composable -private fun StartLiveLocationActionView( - action: AsyncAction, - onActionSuccess: () -> Unit, - modifier: Modifier = Modifier, -) { - 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) { onActionSuccess() } - } - else -> Unit - } - } -} - @Composable private fun BottomSheetContent( cameraState: CameraState, @@ -246,7 +202,7 @@ private fun BottomSheetContent( } if (state.canShareLiveLocation) { ShareLiveLocationItem { - state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) + state.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker) } } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/LiveLocationShareComparator.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/LiveLocationShareComparator.kt deleted file mode 100644 index 41b9bea4ca..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/LiveLocationShareComparator.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.location.impl.show - -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.location.LiveLocationShare - -class LiveLocationShareComparator(private val currentUser: UserId) : Comparator { - override fun compare(p0: LiveLocationShare, p1: LiveLocationShare): Int { - val p0IsCurrentUser = p0.userId == currentUser - val p1IsCurrentUser = p1.userId == currentUser - if (p0IsCurrentUser != p1IsCurrentUser) return if (p0IsCurrentUser) -1 else 1 - return p1.startTimestamp.compareTo(p0.startTimestamp) - } -} 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 34132dccf3..6a3e3521e0 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,5 +17,4 @@ 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 207b4b01dd..a2c9a3702d 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 @@ -13,19 +13,14 @@ import androidx.compose.runtime.LaunchedEffect 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 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.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 @@ -34,21 +29,14 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS import io.element.android.features.location.impl.common.toDialogState import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.core.meta.BuildMeta 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.room.JoinedRoom -import io.element.android.libraries.matrix.api.room.getBestName -import io.element.android.libraries.matrix.api.room.joinedRoomMembers import io.element.android.libraries.ui.strings.CommonStrings 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( @@ -58,8 +46,6 @@ class ShowLocationPresenter( private val buildMeta: BuildMeta, private val dateFormatter: DateFormatter, private val stringProvider: StringProvider, - private val joinedRoom: JoinedRoom, - private val liveLocationShareManager: ActiveLiveLocationShareManager, ) : Presenter { @AssistedFactory fun interface Factory { @@ -70,7 +56,6 @@ 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 } } @@ -91,7 +76,7 @@ class ShowLocationPresenter( } is ShowLocationEvent.TrackMyLocation -> { if (event.enabled) { - val locationConstraints = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) + val locationConstraints = checkLocationConstraints(permissionsState, locationActions) isTrackMyLocation = locationConstraints is LocationConstraintsCheck.Success dialogState = locationConstraints.toDialogState() } else { @@ -108,15 +93,12 @@ class ShowLocationPresenter( dialogState = LocationConstraintsDialogState.None } ShowLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions) - ShowLocationEvent.StopLocationSharing -> coroutineScope.launch { - liveLocationShareManager.stopShare(joinedRoom.roomId) - } } } - val locationShares = when (mode) { - is ShowLocationMode.Static -> { - remember { + val locationShares = remember { + when (mode) { + is ShowLocationMode.Static -> { val relativeTime = dateFormatter.format(timestamp = mode.timestamp, mode = DateFormatterMode.Full, useRelative = true) val formattedTimestamp = stringProvider.getString( CommonStrings.screen_static_location_sheet_timestamp_description, @@ -136,64 +118,18 @@ class ShowLocationPresenter( location = mode.location, isLive = false, assetType = mode.assetType, - isOwnUser = mode.senderId == joinedRoom.sessionId ) ) } + ShowLocationMode.Live -> persistentListOf() } - is ShowLocationMode.Live -> { - produceState(persistentListOf()) { - val comparator = LiveLocationShareComparator(currentUser = joinedRoom.sessionId) - 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, - 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 } } return ShowLocationState( dialogState = dialogState, locationShares = locationShares, - focusedLocation = focusedLocation, hasLocationPermission = permissionsState.isAnyGranted, isTrackMyLocation = isTrackMyLocation, - isLive = mode is ShowLocationMode.Live, appName = appName, 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 720697cbf7..9494db12ec 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 @@ -18,16 +18,14 @@ import io.element.android.libraries.matrix.api.room.location.AssetType import kotlinx.collections.immutable.ImmutableList data class ShowLocationState( - val isLive: Boolean, val dialogState: LocationConstraintsDialogState, val locationShares: ImmutableList, - val focusedLocation: LocationShareItem?, val hasLocationPermission: Boolean, val isTrackMyLocation: Boolean, val appName: String, val eventSink: (ShowLocationEvent) -> Unit, ) { - val isSheetDraggable = isLive && locationShares.isNotEmpty() + val isSheetDraggable = locationShares.any { item -> item.isLive } } data class LocationShareItem( @@ -38,10 +36,7 @@ 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 1c7b9a3160..8bee410715 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 @@ -13,7 +13,6 @@ import io.element.android.features.location.api.Location import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState 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 @@ -22,8 +21,6 @@ class ShowLocationStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aShowLocationState(), - aShowLocationState(isLive = true), - aShowLocationState(isLive = true, locationShares = emptyList()), aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, ), @@ -47,10 +44,8 @@ class ShowLocationStateProvider : PreviewParameterProvider { private const val APP_NAME = "ApplicationName" fun aShowLocationState( - isLive: Boolean = false, constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None, - locationShares: List = listOf(aLocationShareItem(isLive = isLive)), - focusedLocation: LocationShareItem? = locationShares.firstOrNull(), + locationShares: List = listOf(aLocationShareItem()), hasLocationPermission: Boolean = false, isTrackMyLocation: Boolean = false, appName: String = APP_NAME, @@ -59,29 +54,26 @@ fun aShowLocationState( return ShowLocationState( dialogState = constraintsDialogState, locationShares = locationShares.toImmutableList(), - focusedLocation = focusedLocation, hasLocationPermission = hasLocationPermission, isTrackMyLocation = isTrackMyLocation, appName = appName, - isLive = isLive, eventSink = eventSink, ) } fun aLocationShareItem( userId: UserId = UserId("@alice:matrix.org"), - displayName: String = USER_NAME_ALICE, + displayName: String = "Alice", avatarData: AvatarData = AvatarData( id = userId.value, name = displayName, url = null, size = AvatarSize.UserListItem, ), - isLive: Boolean = false, - assetType: AssetType? = null, formattedTimestamp: String = "Shared 1 min ago", location: Location = Location(1.23, 2.34, 4f), - isOwnUser: Boolean = false, + isLive: Boolean = false, + assetType: AssetType? = null, ) = LocationShareItem( userId = userId, displayName = displayName, @@ -90,5 +82,4 @@ 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 6766fa6424..ad2d4cb8ca 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 @@ -12,11 +12,8 @@ package io.element.android.features.location.impl.show import androidx.compose.foundation.clickable 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.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetValue @@ -24,15 +21,11 @@ import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -72,33 +65,35 @@ fun ShowLocationView( onDismiss = { state.eventSink(ShowLocationEvent.DismissDialog) }, ) - val cameraState = rememberCameraState(firstPosition = MapDefaults.defaultCameraPosition) - var hasAnimatedToFocusedLocation by remember { mutableStateOf(false) } - LaunchedEffect(state.focusedLocation) { - if (state.focusedLocation != null && !hasAnimatedToFocusedLocation) { - hasAnimatedToFocusedLocation = true - val position = CameraPosition( - target = Position(latitude = state.focusedLocation.location.lat, longitude = state.focusedLocation.location.lon), + val initialPosition = remember { + if (state.locationShares.isEmpty()) { + MapDefaults.defaultCameraPosition + } else { + val firstLocation = state.locationShares.first().location + CameraPosition( + target = Position(latitude = firstLocation.lat, longitude = firstLocation.lon), zoom = MapDefaults.DEFAULT_ZOOM ) - cameraState.position = position } } + val cameraState = rememberCameraState(firstPosition = initialPosition) + val userLocationState = rememberUserLocationState(state.hasLocationPermission) LaunchedEffect(cameraState.isCameraMoving) { if (cameraState.moveReason == CameraMoveReason.GESTURE) { state.eventSink(ShowLocationEvent.TrackMyLocation(false)) } } - val userLocationState = rememberUserLocationState(state.hasLocationPermission) val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberStandardBottomSheetState(SheetValue.Expanded) + bottomSheetState = rememberStandardBottomSheetState( + initialValue = + if (state.isSheetDraggable) { + SheetValue.PartiallyExpanded + } else { + SheetValue.Expanded + } + ) ) - LaunchedEffect(state.isSheetDraggable) { - if (!state.isSheetDraggable) { - scaffoldState.bottomSheetState.expand() - } - } MapBottomSheetScaffold( sheetDragHandle = if (state.isSheetDraggable) { { BottomSheetDefaults.DragHandle() } @@ -121,47 +116,29 @@ fun ShowLocationView( }, sheetContent = { sheetPaddings -> val coroutineScope = rememberCoroutineScope() - if (!state.isSheetDraggable) { - // If sheet is draggable the DragHandle has already some padding - Spacer(Modifier.height(20.dp)) - } - if (state.locationShares.isEmpty()) { - Text( - text = stringResource(CommonStrings.screen_live_location_sheet_nobody_sharing), - style = ElementTheme.typography.fontBodyLgMedium, - color = ElementTheme.colors.textPrimary, - modifier = Modifier - .fillMaxWidth() - .padding(all = 16.dp), - textAlign = TextAlign.Center, - ) - } else { - Text( - text = stringResource(CommonStrings.screen_static_location_sheet_title), - style = ElementTheme.typography.fontBodyLgMedium, - color = ElementTheme.colors.textPrimary, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - ) - LazyColumn { - items(state.locationShares) { locationShare -> - 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 - ) - coroutineScope.launch { - cameraState.animateTo(finalPosition = position) - } - } + Spacer(Modifier.height(20.dp)) + Text( + text = stringResource(CommonStrings.screen_static_location_sheet_title), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + state.locationShares.forEach { locationShare -> + LocationShareRow( + item = locationShare, + onShareClick = { state.eventSink(ShowLocationEvent.Share(locationShare.location)) }, + 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 ) + coroutineScope.launch { + cameraState.animateTo(finalPosition = position) + } } - } + ) } }, mapContent = { 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 98af57abba..99deeba029 100644 --- a/features/location/impl/src/main/res/values-cs/translations.xml +++ b/features/location/impl/src/main/res/values-cs/translations.xml @@ -1,6 +1,4 @@ - "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 4d4c5d00bc..f15ab0fb2f 100644 --- a/features/location/impl/src/main/res/values-da/translations.xml +++ b/features/location/impl/src/main/res/values-da/translations.xml @@ -1,5 +1,4 @@ - "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-de/translations.xml b/features/location/impl/src/main/res/values-de/translations.xml deleted file mode 100644 index 1a1e208c3b..0000000000 --- a/features/location/impl/src/main/res/values-de/translations.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - "Wie lange soll der Live-Standort geteilt werden?" - diff --git a/features/location/impl/src/main/res/values-el/translations.xml b/features/location/impl/src/main/res/values-el/translations.xml deleted file mode 100644 index 8ae7c58c74..0000000000 --- a/features/location/impl/src/main/res/values-el/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "Το ιστορικό ζωντανής τοποθεσίας σας θα αποθηκευτεί στην αίθουσα και θα είναι ορατό στα μέλη μετά το τέλος της συνεδρίας." - "Επιλέξτε για πόσο χρονικό διάστημα θα κοινοποιείτε την τρέχουσα τοποθεσία σας." - diff --git a/features/location/impl/src/main/res/values-et/translations.xml b/features/location/impl/src/main/res/values-et/translations.xml deleted file mode 100644 index 73847bcb43..0000000000 --- a/features/location/impl/src/main/res/values-et/translations.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - "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 b35d11cd49..bc7e84e7b0 100644 --- a/features/location/impl/src/main/res/values-fi/translations.xml +++ b/features/location/impl/src/main/res/values-fi/translations.xml @@ -1,6 +1,4 @@ - "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-hr/translations.xml b/features/location/impl/src/main/res/values-hr/translations.xml deleted file mode 100644 index 0a294ade1b..0000000000 --- a/features/location/impl/src/main/res/values-hr/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "Vaša povijest lokacije uživo bit će pohranjena u sobi i vidljiva članovima nakon završetka sesije." - "Odaberite koliko dugo želite dijeliti svoju lokaciju uživo." - 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 0ccb2a2413..b89965485f 100644 --- a/features/location/impl/src/main/res/values-hu/translations.xml +++ b/features/location/impl/src/main/res/values-hu/translations.xml @@ -2,5 +2,4 @@ "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 44adc1e3c5..235a9eba4a 100644 --- a/features/location/impl/src/main/res/values-it/translations.xml +++ b/features/location/impl/src/main/res/values-it/translations.xml @@ -2,5 +2,4 @@ "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 1060120ee5..971693ef11 100644 --- a/features/location/impl/src/main/res/values-ja/translations.xml +++ b/features/location/impl/src/main/res/values-ja/translations.xml @@ -2,5 +2,4 @@ "ライブ位置情報の履歴はルームに保管され、メンバーは後から確認することもできます。" "ライブ位置情報を共有する期間を選択してください。" - "このルームでライブ位置情報を共有する権限がありません。" diff --git a/features/location/impl/src/main/res/values-pl/translations.xml b/features/location/impl/src/main/res/values-pl/translations.xml deleted file mode 100644 index c480d0f43b..0000000000 --- a/features/location/impl/src/main/res/values-pl/translations.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - "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 deleted file mode 100644 index 85e665647a..0000000000 --- a/features/location/impl/src/main/res/values-ro/translations.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - "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 97b279621c..3c496f1d39 100644 --- a/features/location/impl/src/main/res/values-ru/translations.xml +++ b/features/location/impl/src/main/res/values-ru/translations.xml @@ -2,5 +2,4 @@ "История вашего местоположения в режиме реального времени будет сохранена в комнате и станет доступна участникам после окончания сессии." "Выберите, как долго вы будете делиться своим местоположением в режиме реального времени." - "У тебя нет прав на то, чтобы делиться своим текущим местоположением в этой комнате" diff --git a/features/location/impl/src/main/res/values-uk/translations.xml b/features/location/impl/src/main/res/values-uk/translations.xml deleted file mode 100644 index 3c8155817a..0000000000 --- a/features/location/impl/src/main/res/values-uk/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "Ваша історія поточного місцезнаходження зберігатиметься у кімнаті та буде доступна учасникам після завершення сеансу." - "Виберіть, як довго ділитися своїм місцезнаходженням." - diff --git a/features/location/impl/src/main/res/values-uz/translations.xml b/features/location/impl/src/main/res/values-uz/translations.xml deleted file mode 100644 index 2b10808d85..0000000000 --- a/features/location/impl/src/main/res/values-uz/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "Jonli joylashuv tarixingiz chat-xonada saqlanadi va sessiya tugaganidan keyin homiylarga ko‘rinadi." - "Jonli joylashuvingiz qancha vaqt ulashilishini tanlang." - diff --git a/features/location/impl/src/main/res/values-zh-rTW/translations.xml b/features/location/impl/src/main/res/values-zh-rTW/translations.xml deleted file mode 100644 index 27f507732d..0000000000 --- a/features/location/impl/src/main/res/values-zh-rTW/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "您的即時位置歷史將儲存於聊天室中,並在工作階段結束後對其他成員可見。" - "選擇分享即時位置的時間長度。" - diff --git a/features/location/impl/src/main/res/values-zh/translations.xml b/features/location/impl/src/main/res/values-zh/translations.xml deleted file mode 100644 index 86f2f2a9da..0000000000 --- a/features/location/impl/src/main/res/values-zh/translations.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - "你的实时位置历史将存储在房间中,并于会话结束后对其他成员可见。" - "选择共享实时位置的时长。" - "你无权在此房内共享实时位置。" - diff --git a/features/location/impl/src/main/res/values/localazy.xml b/features/location/impl/src/main/res/values/localazy.xml index 975bb3c6ea..ac2ff4b2a0 100644 --- a/features/location/impl/src/main/res/values/localazy.xml +++ b/features/location/impl/src/main/res/values/localazy.xml @@ -2,5 +2,4 @@ "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/common/LocationConstraintsCheckTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheckTest.kt index debe95b464..c8e1f21a48 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, SendLiveLocationPermissions.GRANTED) + val result = checkLocationConstraints(permissionsState, locationActions) assertThat(result).isEqualTo(LocationConstraintsCheck.Success) } @@ -33,7 +33,7 @@ class LocationConstraintsCheckTest { ) val locationActions = FakeLocationActions(isLocationEnabled = true) - val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) + val result = checkLocationConstraints(permissionsState, locationActions) assertThat(result).isEqualTo(LocationConstraintsCheck.Success) } @@ -45,7 +45,7 @@ class LocationConstraintsCheckTest { ) val locationActions = FakeLocationActions(isLocationEnabled = false) - val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) + val result = checkLocationConstraints(permissionsState, locationActions) assertThat(result).isEqualTo(LocationConstraintsCheck.LocationServiceDisabled) } @@ -58,7 +58,7 @@ class LocationConstraintsCheckTest { ) val locationActions = FakeLocationActions(isLocationEnabled = true) - val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) + val result = checkLocationConstraints(permissionsState, locationActions) assertThat(result).isEqualTo(LocationConstraintsCheck.PermissionRationale) } @@ -71,20 +71,8 @@ class LocationConstraintsCheckTest { ) val locationActions = FakeLocationActions(isLocationEnabled = true) - val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) + val result = checkLocationConstraints(permissionsState, locationActions) 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) - - assertThat(result).isEqualTo(LocationConstraintsCheck.NotEnoughPowerLevel) - } } 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 deleted file mode 100644 index 85f0e1c33f..0000000000 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/live/DefaultActiveLiveLocationShareManagerTest.kt +++ /dev/null @@ -1,488 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 deleted file mode 100644 index f74322b4c5..0000000000 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/live/LiveLocationSharingCoordinatorTest.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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) { error("boom") } - coordinator.register(A_SESSION_ID_2) { location -> delivered += location } - 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) { location -> delivered += location } - - 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) { location -> delivered += location } - - 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) { location -> delivered += location } - - 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() - } -} 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 6f20e296d9..edd000e02c 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 @@ -13,18 +13,15 @@ import com.bumble.appyx.core.modality.BuildContext import com.google.common.truth.Truth.assertThat 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 @@ -33,29 +30,24 @@ class DefaultShareLocationEntryPointTest { val instantTaskExecutorRule = InstantTaskExecutorRule() @Test - fun `test node builder`() = runTest { + fun `test node builder`() { 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 = room, + room = FakeJoinedRoom(), 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, - ), ) }, 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 1bc4fce586..46bc4a55df 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt @@ -10,9 +10,6 @@ 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.test @@ -25,46 +22,28 @@ import io.element.android.features.location.impl.common.permissions.FakePermissi 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 @@ -75,28 +54,25 @@ 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 durationFormatter = FakeDurationFormatter() - private fun TestScope.createShareLocationPresenter( + private fun createShareLocationPresenter( joinedRoom: JoinedRoom = FakeJoinedRoom(), - timelineMode: Timeline.Mode = Timeline.Mode.Live, locationActions: FakeLocationActions = fakeLocationActions, - liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), - liveLocationStore: LiveLocationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId), ): ShareLocationPresenter = ShareLocationPresenter( permissionsPresenterFactory = { fakePermissionsPresenter }, room = joinedRoom, - timelineMode = timelineMode, + timelineMode = Timeline.Mode.Live, analyticsService = fakeAnalyticsService, messageComposerContext = fakeMessageComposerContext, locationActions = locationActions, buildMeta = fakeBuildMeta, + featureFlagService = fakeFeatureFlagService, client = fakeMatrixClient, durationFormatter = durationFormatter, - liveLocationShareManager = liveLocationShareManager, - liveLocationStore = liveLocationStore, ) @Test @@ -320,15 +296,7 @@ class ShareLocationPresenterTest { @Test fun `ShowLiveLocationDurationPicker shows duration dialog when constraints pass`() = runTest { - val joinedRoom = FakeJoinedRoom( - baseRoom = FakeBaseRoom( - roomPermissions = grantedSendLiveLocationPermissions() - ) - ) - val locationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId).apply { - setAcceptedLiveLocationDisclaimer().getOrThrow() - } - val shareLocationPresenter = createShareLocationPresenter(joinedRoom = joinedRoom, liveLocationStore = locationStore) + val shareLocationPresenter = createShareLocationPresenter() fakePermissionsPresenter.givenState( aPermissionsState( permissions = PermissionsState.Permissions.AllGranted, @@ -339,7 +307,7 @@ class ShareLocationPresenterTest { shareLocationPresenter.test { skipItems(1) val initialState = awaitItem() - initialState.eventSink(ShareLocationEvent.InitiateLiveLocationShare) + initialState.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker) val durationDialogState = awaitItem() assertThat(durationDialogState.dialogState).isInstanceOf(ShareLocationState.Dialog.LiveLocationDurations::class.java) @@ -347,155 +315,9 @@ class ShareLocationPresenterTest { } } - @Test - fun `ShowLiveLocationDurationPicker shows disclaimer when acceptance is missing`() = runTest { - val presenter = createShareLocationPresenter() - fakePermissionsPresenter.givenState( - aPermissionsState( - permissions = PermissionsState.Permissions.AllGranted, - shouldShowRationale = false, - ) - ) - - presenter.test { - skipItems(1) - val state = awaitItem() - - 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 { - skipItems(1) - val state = awaitItem() - 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 { - skipItems(1) - val state = awaitItem() - 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 { - skipItems(1) - val state = awaitItem() - - 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"))) - 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 { - skipItems(1) - val state = awaitItem() - - 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, - ) + val shareLocationPresenter = createShareLocationPresenter() fakePermissionsPresenter.givenState( aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, @@ -510,7 +332,7 @@ class ShareLocationPresenterTest { initialState.eventSink(ShareLocationEvent.DismissDialog) val dismissedState = awaitItem() - dismissedState.eventSink(ShareLocationEvent.InitiateLiveLocationShare) + dismissedState.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker) val constraintDialogState = awaitItem() assertThat(constraintDialogState.dialogState).isEqualTo( @@ -625,86 +447,4 @@ 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 { - skipItems(1) - val state = awaitItem() - 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 { - skipItems(1) - val state = awaitItem() - 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 { - skipItems(1) - val state = awaitItem() - assertThat(state.canShareLiveLocation).isFalse() - } - } } - -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 370ccac8ab..317fbf8fed 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,18 +5,15 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.libraries.testtags.TestTags @@ -26,98 +23,102 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShareLocationViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `test back action`() = runAndroidComposeUiTest { + fun `test back action`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setShareLocationView( + rule.setShareLocationView( state = aShareLocationState( eventSink = eventsRecorder ), navigateUp = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `test fab click`() = runAndroidComposeUiTest { + fun `test fab click`() { val eventsRecorder = EventsRecorder() - setShareLocationView( + rule.setShareLocationView( aShareLocationState( eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - onNodeWithTag(TestTags.floatingActionButton.value).performClick() + rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() eventsRecorder.assertSingle(ShareLocationEvent.StartTrackingUserLocation) } @Test - fun `when permission denied is displayed user can open the settings`() = runAndroidComposeUiTest { + fun `when permission denied is displayed user can open the settings`() { val eventsRecorder = EventsRecorder() - setShareLocationView( + rule.setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShareLocationEvent.OpenAppSettings) } @Test - fun `when permission denied is displayed user can close the dialog`() = runAndroidComposeUiTest { + fun `when permission denied is displayed user can close the dialog`() { val eventsRecorder = EventsRecorder() - setShareLocationView( + rule.setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) } @Test - fun `when permission rationale is displayed user can request permissions`() = runAndroidComposeUiTest { + fun `when permission rationale is displayed user can request permissions`() { val eventsRecorder = EventsRecorder() - setShareLocationView( + rule.setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShareLocationEvent.RequestPermissions) } @Test - fun `when permission rationale is displayed user can close the dialog`() = runAndroidComposeUiTest { + fun `when permission rationale is displayed user can close the dialog`() { val eventsRecorder = EventsRecorder() - setShareLocationView( + rule.setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) } @Test - fun `when location service disabled is displayed user can open location settings`() = runAndroidComposeUiTest { + fun `when location service disabled is displayed user can open location settings`() { val eventsRecorder = EventsRecorder() - setShareLocationView( + rule.setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled), hasLocationPermission = true, @@ -125,14 +126,14 @@ class ShareLocationViewTest { ), navigateUp = EnsureNeverCalled(), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShareLocationEvent.OpenLocationSettings) } @Test - fun `when location service disabled is displayed user can close the dialog`() = runAndroidComposeUiTest { + fun `when location service disabled is displayed user can close the dialog`() { val eventsRecorder = EventsRecorder() - setShareLocationView( + rule.setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled), hasLocationPermission = true, @@ -140,44 +141,12 @@ class ShareLocationViewTest { ), navigateUp = EnsureNeverCalled(), ) - 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) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) } } -private fun AndroidComposeUiTest.setShareLocationView( +private fun AndroidComposeTestRule.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 985dcc1f9c..451531fc7e 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 @@ -16,11 +16,9 @@ import io.element.android.features.location.api.ShowLocationEntryPoint import io.element.android.features.location.api.ShowLocationMode 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.core.aBuildMeta -import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.node.TestParentNode @@ -35,7 +33,6 @@ class DefaultShowLocationEntryPointTest { fun `test node builder`() { val entryPoint = DefaultShowLocationEntryPoint() val parentNode = TestParentNode.create { buildContext, plugins -> - val joinedRoom = FakeJoinedRoom() ShowLocationNode( buildContext = buildContext, plugins = plugins, @@ -46,9 +43,7 @@ class DefaultShowLocationEntryPointTest { locationActions = FakeLocationActions(), buildMeta = aBuildMeta(), dateFormatter = FakeDateFormatter(), - stringProvider = FakeStringProvider(), - joinedRoom = joinedRoom, - liveLocationShareManager = FakeActiveLiveLocationShareManager(), + stringProvider = FakeStringProvider() ) }, 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 deleted file mode 100644 index 0b8e04abf8..0000000000 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/LiveLocationShareComparatorTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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.test.room.location.aLiveLocationShare -import org.junit.Test - -class LiveLocationShareComparatorTest { - private val currentUser = UserId("@me:matrix.org") - private val comparator = LiveLocationShareComparator(currentUser) - - @Test - fun `compare returns zero when comparing the same current user share`() { - val share = aLiveLocationShare(userId = currentUser, startTimestamp = 123L) - - val result = comparator.compare(share, share) - - assertThat(result).isEqualTo(0) - } - - @Test - fun `compare orders current user share before another user share`() { - val otherShare = aLiveLocationShare(userId = UserId("@alice:matrix.org"), startTimestamp = 200L) - val currentUserShare = aLiveLocationShare(userId = currentUser, startTimestamp = 100L) - - val sortedShares = listOf(otherShare, currentUserShare).sortedWith(comparator) - - assertThat(sortedShares).containsExactly(currentUserShare, otherShare).inOrder() - } - - @Test - fun `compare orders current user shares by newest start timestamp first`() { - val newerShare = aLiveLocationShare(userId = currentUser, startTimestamp = 200L) - val olderShare = aLiveLocationShare(userId = currentUser, startTimestamp = 100L) - - val sortedShares = listOf(olderShare, newerShare).sortedWith(comparator) - - assertThat(sortedShares).containsExactly(newerShare, olderShare).inOrder() - } - - @Test - fun `compare orders non current user shares by newest start timestamp first`() { - val newerShare = aLiveLocationShare(userId = UserId("@alice:matrix.org"), startTimestamp = 200L) - val olderShare = aLiveLocationShare(userId = UserId("@bob:matrix.org"), startTimestamp = 100L) - - val sortedShares = listOf(olderShare, newerShare).sortedWith(comparator) - - assertThat(sortedShares).containsExactly(newerShare, olderShare).inOrder() - } -} 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 c1f9f6487e..931dd55cea 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 @@ -20,26 +20,17 @@ import io.element.android.features.location.impl.common.permissions.FakePermissi 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.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.LiveLocationShare 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 -@OptIn(ExperimentalCoroutinesApi::class) class ShowLocationPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -60,17 +51,13 @@ class ShowLocationPresenterTest { assetType = null, ), locationActions: FakeLocationActions = fakeLocationActions, - joinedRoom: JoinedRoom = FakeJoinedRoom(), - liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), ) = ShowLocationPresenter( mode = mode, permissionsPresenterFactory = { fakePermissionsPresenter }, locationActions = locationActions, buildMeta = fakeBuildMeta, dateFormatter = fakeDateFormatter, - stringProvider = FakeStringProvider(), - joinedRoom = joinedRoom, - liveLocationShareManager = liveLocationShareManager, + stringProvider = FakeStringProvider() ) @Test @@ -208,7 +195,7 @@ class ShowLocationPresenterTest { ) ) val presenter = createShowLocationPresenter() - presenter.test { + presenter.test { // Skip initial state val initialState = awaitItem() @@ -331,139 +318,4 @@ class ShowLocationPresenterTest { assertThat(fakeLocationActions.openLocationSettingsInvocationsCount).isEqualTo(1) } } - - @Test - fun `live mode emits empty location shares initially`() = runTest { - val presenter = createShowLocationPresenter( - mode = ShowLocationMode.Live(senderId = UserId("@alice:matrix.org")), - joinedRoom = FakeJoinedRoom(), - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.locationShares).isEmpty() - assertThat(initialState.isSheetDraggable).isFalse() - } - } - - @Test - fun `live mode collects live shares from room`() = runTest { - val userId = UserId("@bob:matrix.org") - val liveSharesFlow = MutableStateFlow( - listOf( - aLiveLocationShare(userId = userId) - ) - ) - val fakeRoom = FakeJoinedRoom(liveLocationSharesFlow = liveSharesFlow) - - val presenter = createShowLocationPresenter( - mode = ShowLocationMode.Live(senderId = userId), - joinedRoom = fakeRoom, - ) - presenter.test { - // Skip initial empty state from collectAsState(initial = emptyList()) - skipItems(1) - val state = awaitItem() - - assertThat(state.locationShares).hasSize(1) - val item = state.locationShares.first() - assertThat(item.userId).isEqualTo(userId) - assertThat(item.location.lat).isEqualTo(48.8584) - assertThat(item.location.lon).isEqualTo(2.2945) - assertThat(item.isLive).isTrue() - assertThat(state.isSheetDraggable).isTrue() - } - } - - @Test - fun `live mode handles invalid geo uri gracefully`() = runTest { - val validUserId = UserId("@alice:matrix.org") - val invalidUserId = UserId("@bob:matrix.org") - val liveSharesFlow = MutableStateFlow( - listOf( - aLiveLocationShare(userId = validUserId), - aLiveLocationShare(userId = invalidUserId, geoUri = "invalid-geo-uri"), - ) - ) - val fakeRoom = FakeJoinedRoom(liveLocationSharesFlow = liveSharesFlow) - - val presenter = createShowLocationPresenter( - mode = ShowLocationMode.Live(senderId = validUserId), - joinedRoom = fakeRoom, - ) - presenter.test { - // Skip initial empty state from collectAsState(initial = emptyList()) - skipItems(1) - val state = awaitItem() - - // Only the valid location share should be present - assertThat(state.locationShares).hasSize(1) - assertThat(state.locationShares.first().userId).isEqualTo(validUserId) - } - } - - @Test - fun `live mode updates when shares change`() = runTest { - val userId = UserId("@bob:matrix.org") - val liveSharesFlow = MutableStateFlow(emptyList()) - val fakeRoom = FakeJoinedRoom(liveLocationSharesFlow = liveSharesFlow) - - val presenter = createShowLocationPresenter( - mode = ShowLocationMode.Live(senderId = userId), - joinedRoom = fakeRoom, - ) - presenter.test { - // Initial state is empty - val initialState = awaitItem() - assertThat(initialState.locationShares).isEmpty() - - // Emit a new live share - liveSharesFlow.value = listOf( - aLiveLocationShare(userId = userId) - ) - - val updatedState = awaitItem() - assertThat(updatedState.locationShares).hasSize(1) - assertThat(updatedState.locationShares.first().userId).isEqualTo(userId) - } - } - - @Test - fun `static mode emits location share with correct data`() = runTest { - val senderId = UserId("@alice:matrix.org") - val senderName = "Alice" - val avatarUrl = "https://example.com/avatar.png" - val mode = ShowLocationMode.Static( - location = location, - senderName = senderName, - senderId = senderId, - senderAvatarUrl = avatarUrl, - timestamp = 0L, - assetType = AssetType.SENDER, - ) - - val presenter = createShowLocationPresenter(mode = mode) - presenter.test { - val state = awaitItem() - assertThat(state.locationShares).hasSize(1) - - val item = state.locationShares.first() - assertThat(item.userId).isEqualTo(senderId) - assertThat(item.displayName).isEqualTo(senderName) - assertThat(item.location).isEqualTo(location) - assertThat(item.isLive).isFalse() - assertThat(item.assetType).isEqualTo(AssetType.SENDER) - assertThat(item.avatarData.id).isEqualTo(senderId.value) - assertThat(item.avatarData.name).isEqualTo(senderName) - assertThat(item.avatarData.url).isEqualTo(avatarUrl) - } - } - - @Test - fun `static mode has non-draggable sheet`() = runTest { - val presenter = createShowLocationPresenter() - presenter.test { - val state = awaitItem() - assertThat(state.isSheetDraggable).isFalse() - } - } } 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 45ed894f97..fecbbdbf89 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,19 +6,16 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.performClick -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.location.api.Location import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState @@ -29,111 +26,115 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShowLocationViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `test back action`() = runAndroidComposeUiTest { + fun `test back action`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setShowLocationView( + rule.setShowLocationView( state = aShowLocationState( eventSink = eventsRecorder ), onBackClick = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `test share action`() = runAndroidComposeUiTest { + fun `test share action`() { val eventsRecorder = EventsRecorder() - setShowLocationView( + rule.setShowLocationView( aShowLocationState( eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - val shareContentDescription = activity!!.getString(CommonStrings.action_share) - onNodeWithContentDescription(shareContentDescription).performClick() + val shareContentDescription = rule.activity.getString(CommonStrings.action_share) + rule.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`() = runAndroidComposeUiTest { + fun `test fab click`() { val eventsRecorder = EventsRecorder() - setShowLocationView( + rule.setShowLocationView( aShowLocationState( eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - onNodeWithTag(TestTags.floatingActionButton.value).performClick() + rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() eventsRecorder.assertSingle(ShowLocationEvent.TrackMyLocation(true)) } @Test - fun `when permission denied is displayed user can open the settings`() = runAndroidComposeUiTest { + fun `when permission denied is displayed user can open the settings`() { val eventsRecorder = EventsRecorder() - setShowLocationView( + rule.setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShowLocationEvent.OpenAppSettings) } @Test - fun `when permission denied is displayed user can close the dialog`() = runAndroidComposeUiTest { + fun `when permission denied is displayed user can close the dialog`() { val eventsRecorder = EventsRecorder() - setShowLocationView( + rule.setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShowLocationEvent.DismissDialog) } @Test - fun `when permission rationale is displayed user can request permissions`() = runAndroidComposeUiTest { + fun `when permission rationale is displayed user can request permissions`() { val eventsRecorder = EventsRecorder() - setShowLocationView( + rule.setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionRationale, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShowLocationEvent.RequestPermissions) } @Test - fun `when permission rationale is displayed user can close the dialog`() = runAndroidComposeUiTest { + fun `when permission rationale is displayed user can close the dialog`() { val eventsRecorder = EventsRecorder() - setShowLocationView( + rule.setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionRationale, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShowLocationEvent.DismissDialog) } } -private fun AndroidComposeUiTest.setShowLocationView( +private fun AndroidComposeTestRule.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 deleted file mode 100644 index c42469e705..0000000000 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/store/LiveLocationStoreTest.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 e51737d40c..f84e8ba772 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 deleted file mode 100644 index 255c181ac1..0000000000 --- a/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeActiveLiveLocationShareManager.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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/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 49d299b3f9..7ac42feda6 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, - private val biometricAuthenticatorManager: BiometricAuthenticatorManager, + biometricAuthenticatorManager: BiometricAuthenticatorManager, ) : LockScreenService { private val _lockState = MutableStateFlow(LockScreenLockState.Unlocked) override val lockState: StateFlow = _lockState @@ -81,7 +81,6 @@ 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 d18d9b73b7..a96c713ff2 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 - suspend fun setup() + fun setup() suspend fun authenticate(): AuthenticationResult } class NoopBiometricAuthentication : BiometricAuthenticator { override val isActive: Boolean = false - override suspend fun setup() = Unit + override fun setup() = Unit override suspend fun authenticate() = BiometricAuthenticator.AuthenticationResult.Failure() } @@ -58,7 +58,7 @@ class DefaultBiometricAuthentication( private var cryptoObject: CryptoObject? = null - override suspend fun setup() { + override fun setup() { try { val secretKey = ensureKey() val cipher = encryptionDecryptionService.createEncryptionCipher(secretKey) @@ -86,7 +86,7 @@ class DefaultBiometricAuthentication( } @Throws(KeyPermanentlyInvalidatedException::class) - private suspend fun ensureKey() = secretKeyRepository.getOrCreateKey(keyAlias, true).also { + private fun ensureKey() = secretKeyRepository.getOrCreateKey(keyAlias, true).also { encryptionDecryptionService.createEncryptionCipher(it) } } 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 2ea0ed7d05..9917845725 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 @@ -24,11 +24,6 @@ interface BiometricAuthenticatorManager { 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. */ 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 117323ec0a..8bb044fd06 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 @@ -80,7 +80,10 @@ class DefaultBiometricAuthenticatorManager( private val internalCallback = object : DefaultBiometricUnlockCallback() { override fun onBiometricSetupError() { - coroutineScope.launch { disable() } + coroutineScope.launch { + lockScreenStore.setIsBiometricUnlockAllowed(false) + secretKeyRepository.deleteKey(SECRET_KEY_ALIAS) + } } } @@ -117,11 +120,6 @@ class DefaultBiometricAuthenticatorManager( ) } - override suspend fun disable() { - lockScreenStore.setIsBiometricUnlockAllowed(false) - secretKeyRepository.deleteKey(SECRET_KEY_ALIAS) - } - @Composable private fun rememberBiometricAuthenticator( isAvailable: Boolean, 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 d699357933..091432044a 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,13 +16,9 @@ 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 -internal const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE" +private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE" @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) @@ -33,8 +29,6 @@ class DefaultPinCodeManager( ) : PinCodeManager { private val callbacks = CopyOnWriteArrayList() - private val migrationMutex = Mutex() - override fun addCallback(callback: PinCodeManager.Callback) { callbacks.add(callback) } @@ -44,20 +38,11 @@ class DefaultPinCodeManager( } override fun hasPinCode(): Flow { - 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) - } - } - } + return lockScreenStore.hasPinCode() } - override suspend fun getPinCodeSize(): Int? { - val encryptedPinCode = lockScreenStore.getEncryptedCode() ?: return null + override suspend fun getPinCodeSize(): Int { + val encryptedPinCode = lockScreenStore.getEncryptedCode() ?: return 0 val secretKey = secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, false) val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) return decryptedPinCode.size @@ -94,7 +79,6 @@ 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 350631a233..9282f3e7df 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 null if no pin code is saved. + * @return the size of the saved pin code. */ - 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/LockScreenSettingsEvent.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt similarity index 62% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvent.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt index c7437912eb..2d62427e02 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvent.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt @@ -8,9 +8,9 @@ package io.element.android.features.lockscreen.impl.settings -sealed interface LockScreenSettingsEvent { - data object OnRemovePin : LockScreenSettingsEvent - data object ConfirmRemovePin : LockScreenSettingsEvent - data object CancelRemovePin : LockScreenSettingsEvent - data object ToggleBiometricAllowed : LockScreenSettingsEvent +sealed interface LockScreenSettingsEvents { + data object OnRemovePin : LockScreenSettingsEvents + data object ConfirmRemovePin : LockScreenSettingsEvents + data object CancelRemovePin : LockScreenSettingsEvents + data object ToggleBiometricAllowed : LockScreenSettingsEvents } 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 17a0213f63..589794bde0 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,20 +51,19 @@ class LockScreenSettingsPresenter( val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator() - fun handleEvent(event: LockScreenSettingsEvent) { + fun handleEvent(event: LockScreenSettingsEvents) { when (event) { - LockScreenSettingsEvent.CancelRemovePin -> showRemovePinConfirmation = false - LockScreenSettingsEvent.ConfirmRemovePin -> { + LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false + LockScreenSettingsEvents.ConfirmRemovePin -> { coroutineScope.launch { if (showRemovePinConfirmation) { showRemovePinConfirmation = false pinCodeManager.deletePinCode() - biometricAuthenticatorManager.disable() } } } - LockScreenSettingsEvent.OnRemovePin -> showRemovePinConfirmation = true - LockScreenSettingsEvent.ToggleBiometricAllowed -> { + LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true + LockScreenSettingsEvents.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 62b8d6d4ee..a69d633508 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: (LockScreenSettingsEvent) -> Unit + val eventSink: (LockScreenSettingsEvents) -> 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 e78a5ee002..fe5f20da0d 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(LockScreenSettingsEvent.OnRemovePin) + state.eventSink(LockScreenSettingsEvents.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(LockScreenSettingsEvent.ToggleBiometricAllowed) + state.eventSink(LockScreenSettingsEvents.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(LockScreenSettingsEvent.ConfirmRemovePin) + state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin) }, onDismiss = { - state.eventSink(LockScreenSettingsEvent.CancelRemovePin) + state.eventSink(LockScreenSettingsEvents.CancelRemovePin) } ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvent.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt similarity index 68% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvent.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt index d4db46b731..ab8b18642e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvent.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt @@ -8,7 +8,7 @@ package io.element.android.features.lockscreen.impl.setup.biometric -sealed interface SetupBiometricEvent { - data object AllowBiometric : SetupBiometricEvent - data object UsePin : SetupBiometricEvent +sealed interface SetupBiometricEvents { + data object AllowBiometric : SetupBiometricEvents + data object UsePin : SetupBiometricEvents } 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 ce914320bc..3af2a28851 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: SetupBiometricEvent) { + fun handleEvent(event: SetupBiometricEvents) { when (event) { - SetupBiometricEvent.AllowBiometric -> coroutineScope.launch { + SetupBiometricEvents.AllowBiometric -> coroutineScope.launch { biometricUnlock.setup() if (biometricUnlock.authenticate() == BiometricAuthenticator.AuthenticationResult.Success) { lockScreenStore.setIsBiometricUnlockAllowed(true) isBiometricSetupDone = true } } - SetupBiometricEvent.UsePin -> coroutineScope.launch { + SetupBiometricEvents.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 db11b1dc30..2843c028d1 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: (SetupBiometricEvent) -> Unit + val eventSink: (SetupBiometricEvents) -> 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 70a1046e36..35b1ec76c0 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(SetupBiometricEvent.UsePin) + state.eventSink(SetupBiometricEvents.UsePin) } HeaderFooterPage( modifier = modifier.padding(top = 80.dp), @@ -42,8 +42,8 @@ fun SetupBiometricView( }, footer = { SetupBiometricFooter( - onAllowClick = { state.eventSink(SetupBiometricEvent.AllowBiometric) }, - onSkipClick = { state.eventSink(SetupBiometricEvent.UsePin) } + onAllowClick = { state.eventSink(SetupBiometricEvents.AllowBiometric) }, + onSkipClick = { state.eventSink(SetupBiometricEvents.UsePin) } ) }, ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvent.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt similarity index 74% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvent.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt index f0dfdc33f0..276a94b2fc 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvent.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt @@ -8,7 +8,7 @@ package io.element.android.features.lockscreen.impl.setup.pin -sealed interface SetupPinEvent { - data class OnPinEntryChanged(val entryAsText: String, val fromConfirmationStep: Boolean) : SetupPinEvent - data object ClearFailure : SetupPinEvent +sealed interface SetupPinEvents { + data class OnPinEntryChanged(val entryAsText: String, val fromConfirmationStep: Boolean) : SetupPinEvents + data object ClearFailure : SetupPinEvents } 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 d780927d44..ac5b5bd1cc 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: SetupPinEvent) { + fun handleEvent(event: SetupPinEvents) { when (event) { - is SetupPinEvent.OnPinEntryChanged -> { + is SetupPinEvents.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) } } - SetupPinEvent.ClearFailure -> { + SetupPinEvents.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 cf65e63c1b..2d5124d440 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: (SetupPinEvent) -> Unit + val eventSink: (SetupPinEvents) -> 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 508d3c1fbb..5f2320db32 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(SetupPinEvent.OnPinEntryChanged(entry, state.isConfirmationStep)) + state.eventSink(SetupPinEvents.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(SetupPinEvent.ClearFailure) + state.eventSink(SetupPinEvents.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 b41e6a9578..c4558812de 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,6 +8,8 @@ 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. @@ -27,4 +29,9 @@ 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 bce20b2418..6b99d90592 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,6 +70,12 @@ 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 @@ -82,7 +88,5 @@ class PreferencesLockScreenStore( } } - private fun Preferences.getRemainingPinCodeAttemptsNumber() = - this[remainingAttemptsKey]?.coerceIn(0, lockScreenConfig.maxPinCodeAttemptsBeforeLogout) - ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout + private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvent.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt similarity index 61% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvent.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index aa96a2e115..bd9043859f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvent.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.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 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 +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 } 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 c8dd8916f9..5429320fc7 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 @@ -69,13 +69,7 @@ class PinUnlockPresenter( LaunchedEffect(Unit) { suspend { val pinCodeSize = pinCodeManager.getPinCodeSize() - if (pinCodeSize == null) { - // No pin code set, deleted store? Force sign out - showSignOutPrompt = true - error("No pin code size found") - } else { - PinEntry.createEmpty(pinCodeSize) - } + PinEntry.createEmpty(pinCodeSize) }.runCatchingUpdatingState(pinEntryState) } LaunchedEffect(biometricUnlock) { @@ -101,28 +95,28 @@ class PinUnlockPresenter( isUnlocked.value = true } - fun handleEvent(event: PinUnlockEvent) { + fun handleEvent(event: PinUnlockEvents) { when (event) { - is PinUnlockEvent.OnPinKeypadPressed -> { + is PinUnlockEvents.OnPinKeypadPressed -> { pinEntryState.value = pinEntry.process(event.pinKeypadModel) } - PinUnlockEvent.OnForgetPin -> showSignOutPrompt = true - PinUnlockEvent.ClearSignOutPrompt -> showSignOutPrompt = false - PinUnlockEvent.SignOut -> { + PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true + PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false + PinUnlockEvents.SignOut -> { if (showSignOutPrompt) { showSignOutPrompt = false coroutineScope.signOut(signOutAction) } } - PinUnlockEvent.OnUseBiometric -> { + PinUnlockEvents.OnUseBiometric -> { coroutineScope.launch { biometricUnlockResult = biometricUnlock.authenticate() } } - PinUnlockEvent.ClearBiometricError -> { + PinUnlockEvents.ClearBiometricError -> { biometricUnlockResult = null } - is PinUnlockEvent.OnPinEntryChanged -> { + is PinUnlockEvents.OnPinEntryChanged -> { pinEntryState.value = pinEntry.process(event.entryAsText) } } 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 037aa87dec..2bbcbe335c 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 @@ -23,15 +23,11 @@ data class PinUnlockState( val showBiometricUnlock: Boolean, val isUnlocked: Boolean, val biometricUnlockResult: BiometricAuthenticator.AuthenticationResult?, - val eventSink: (PinUnlockEvent) -> Unit + val eventSink: (PinUnlockEvents) -> Unit ) { - val isSignOutPromptCancellable = if (pinEntry.isFailure()) { - false - } else { - when (remainingAttempts) { - is AsyncData.Success -> remainingAttempts.data > 0 - else -> true - } + val isSignOutPromptCancellable = 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 1b8166a8ac..2beb8babe3 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 = AsyncData.Success(PinEntry.createEmpty(4).fillWith("12"))), + aPinUnlockState(pinEntry = PinEntry.createEmpty(4).fillWith("12")), aPinUnlockState(showWrongPinTitle = true), aPinUnlockState(showSignOutPrompt = true), aPinUnlockState(showBiometricUnlock = false), @@ -31,18 +31,11 @@ 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, - ), ) } fun aPinUnlockState( - pinEntry: AsyncData = AsyncData.Success(PinEntry.createEmpty(4)), + pinEntry: PinEntry = PinEntry.createEmpty(4), remainingAttempts: AsyncData = AsyncData.Success(3), showWrongPinTitle: Boolean = false, showSignOutPrompt: Boolean = false, @@ -51,7 +44,7 @@ fun aPinUnlockState( isUnlocked: Boolean = false, signOutAction: AsyncAction = AsyncAction.Uninitialized, ) = PinUnlockState( - pinEntry = pinEntry, + pinEntry = AsyncData.Success(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 6749697b64..659f8c2966 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 @@ -69,7 +69,7 @@ fun PinUnlockView( ) { OnLifecycleEvent { _, event -> when (event) { - Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(PinUnlockEvent.OnUseBiometric) + Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(PinUnlockEvents.OnUseBiometric) else -> Unit } } @@ -78,8 +78,8 @@ fun PinUnlockView( if (state.showSignOutPrompt) { SignOutPrompt( isCancellable = state.isSignOutPromptCancellable, - onSignOut = { state.eventSink(PinUnlockEvent.SignOut) }, - onDismiss = { state.eventSink(PinUnlockEvent.ClearSignOutPrompt) }, + onSignOut = { state.eventSink(PinUnlockEvents.SignOut) }, + onDismiss = { state.eventSink(PinUnlockEvents.ClearSignOutPrompt) }, ) } when (state.signOutAction) { @@ -95,7 +95,7 @@ fun PinUnlockView( if (state.showBiometricUnlockError) { ErrorDialog( content = state.biometricUnlockErrorMessage ?: "", - onSubmit = { state.eventSink(PinUnlockEvent.ClearBiometricError) } + onSubmit = { state.eventSink(PinUnlockEvents.ClearBiometricError) } ) } } @@ -108,10 +108,10 @@ private fun PinUnlockPage( ) { BoxWithConstraints { val commonModifier = Modifier - .fillMaxSize() - .systemBarsPadding() - .imePadding() - .padding(all = 20.dp) + .fillMaxSize() + .systemBarsPadding() + .imePadding() + .padding(all = 20.dp) val header = @Composable { PinUnlockHeader( @@ -125,10 +125,10 @@ private fun PinUnlockPage( modifier = Modifier.padding(top = 24.dp), showBiometricUnlock = state.showBiometricUnlock, onUseBiometric = { - state.eventSink(PinUnlockEvent.OnUseBiometric) + state.eventSink(PinUnlockEvents.OnUseBiometric) }, onForgotPin = { - state.eventSink(PinUnlockEvent.OnForgetPin) + state.eventSink(PinUnlockEvents.OnForgetPin) }, ) } @@ -144,17 +144,17 @@ private fun PinUnlockPage( pinEntry = pinEntry, isSecured = true, onValueChange = { - state.eventSink(PinUnlockEvent.OnPinEntryChanged(it)) + state.eventSink(PinUnlockEvents.OnPinEntryChanged(it)) }, modifier = Modifier - .focusRequester(focusRequester) - .fillMaxWidth() + .focusRequester(focusRequester) + .fillMaxWidth() ) } } else { PinKeypad( onClick = { - state.eventSink(PinUnlockEvent.OnPinKeypadPressed(it)) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) }, maxWidth = constraints.maxWidth, maxHeight = constraints.maxHeight, @@ -217,8 +217,8 @@ private fun PinUnlockCompactView( } BoxWithConstraints( modifier = Modifier - .weight(1f) - .fillMaxHeight(), + .weight(1f) + .fillMaxHeight(), contentAlignment = Alignment.Center, ) { content() @@ -239,9 +239,9 @@ private fun PinUnlockExpandedView( header() BoxWithConstraints( modifier = Modifier - .weight(1f) - .fillMaxWidth() - .padding(top = 40.dp), + .weight(1f) + .fillMaxWidth() + .padding(top = 40.dp), ) { content() } @@ -274,8 +274,8 @@ private fun PinDot( } Box( modifier = Modifier - .size(14.dp) - .background(backgroundColor, CircleShape) + .size(14.dp) + .background(backgroundColor, CircleShape) ) } @@ -311,26 +311,14 @@ private fun PinUnlockHeader( ) Spacer(Modifier.height(8.dp)) val remainingAttempts = state.remainingAttempts.dataOrNull() - val subtitle = when { - state.isUnlocked -> { - // Hide any previous error - "" + 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) } - 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 -> "" + } else { + "" } val subtitleColor = if (state.showWrongPinTitle) { ElementTheme.colors.textCriticalPrimary 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..6209c19be2 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 @@ -30,7 +30,6 @@ import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.designsystem.theme.ElementThemeApp -import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.preferences.api.store.AppPreferencesStore import kotlinx.coroutines.launch @@ -44,7 +43,6 @@ class PinUnlockActivity : AppCompatActivity() { @Inject lateinit var presenter: PinUnlockPresenter @Inject lateinit var lockScreenService: LockScreenService @Inject lateinit var appPreferencesStore: AppPreferencesStore - @Inject lateinit var featureFlagService: FeatureFlagService @Inject lateinit var enterpriseService: EnterpriseService @Inject lateinit var buildMeta: BuildMeta @@ -58,7 +56,6 @@ class PinUnlockActivity : AppCompatActivity() { }.collectAsState(SemanticColorsLightDark.default) ElementThemeApp( appPreferencesStore = appPreferencesStore, - featureFlagService = featureFlagService, compoundLight = colors.light, compoundDark = colors.dark, buildMeta = buildMeta, diff --git a/features/lockscreen/impl/src/main/res/values-ca/translations.xml b/features/lockscreen/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 5637977b97..0000000000 --- a/features/lockscreen/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - "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-de/translations.xml b/features/lockscreen/impl/src/main/res/values-de/translations.xml index e1819583f0..dd74818610 100644 --- a/features/lockscreen/impl/src/main/res/values-de/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-de/translations.xml @@ -23,7 +23,7 @@ Wähle eine einprägsame PIN. Wenn du sie vergisst, wirst du aus der App abgemel "Bitte gib die gleiche PIN wie zuvor ein." "Die PINs stimmen nicht überein" "Um fortzufahren, musst du dich erneut anmelden und eine neue PIN erstellen" - "Dieses Gerät wurde entfernt" + "Du wirst abgemeldet" "Du hast %1$d Versuch, um zu entsperren" "Du hast %1$d Versuche, um zu entsperren" @@ -34,5 +34,5 @@ Wähle eine einprägsame PIN. Wenn du sie vergisst, wirst du aus der App abgemel "Biometrie verwenden" "PIN verwenden" - "Gerät wird entfernt…" + "Abmelden…" 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 7137c09b60..4449479ba6 100644 --- a/features/lockscreen/impl/src/main/res/values-et/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-et/translations.xml @@ -23,7 +23,7 @@ Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvaka "Palun sisesta sama PIN-kood kaks korda" "PIN-koodid ei klapi omavahel" "Jätkamaks pead uuesti sisse logima ja looma uue PIN-koodi" - "See seade on eemaldamisel" + "Sa oled logimas välja" "Sul on lukustuse eemaldamiseks jäänud %1$d katse" "Sul on lukustuse eemaldamiseks jäänud %1$d katset" @@ -34,5 +34,5 @@ Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvaka "Kasuta biomeetriat" "Kasuta PIN-koodi" - "Eemaldan seadet…" + "Logime välja…" 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 0575f22221..56dc91e835 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-hr/translations.xml b/features/lockscreen/impl/src/main/res/values-hr/translations.xml index 232775c798..1a81bcc6cb 100644 --- a/features/lockscreen/impl/src/main/res/values-hr/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-hr/translations.xml @@ -23,7 +23,7 @@ Odaberite nešto nezaboravno. Ako zaboravite ovaj PIN, bit ćete odjavljeni iz a "Unesite dvaput isti PIN" "PIN-ovi se ne podudaraju" "Morat ćete se ponovno prijaviti i izraditi novi PIN da biste mogli nastaviti" - "Ovaj uređaj se uklanja" + "Odjavit ćete se" "Imate %1$d pokušaj otključavanja" "Imate %1$d pokušaja otključavanja" @@ -36,5 +36,5 @@ Odaberite nešto nezaboravno. Ako zaboravite ovaj PIN, bit ćete odjavljeni iz a "Upotrijebi biometriju" "Upotrijebi PIN" - "Uklanjanje uređaja…" + "Odjavljivanje…" 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 e0054cda62..0396f56b0c 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 device dari akun…" + "Mengeluarkan 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 29691987f6..5d61ecb7c6 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 usuwanie urządzenia" + "Trwa wylogowywanie" "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" - "Usuwam urządzenie…" + "Wylogowywanie…" 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 fca6fbcb9e..a6b2516fba 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" - "O teu dispositivo está a ser removido" + "Estás a terminar a sessão" "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 remover dispositivo…" + "A terminar sessão…" diff --git a/features/lockscreen/impl/src/main/res/values-ro/translations.xml b/features/lockscreen/impl/src/main/res/values-ro/translations.xml index 7555d7eb58..d40bfbaece 100644 --- a/features/lockscreen/impl/src/main/res/values-ro/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-ro/translations.xml @@ -23,7 +23,7 @@ Alegeți ceva memorabil. Dacă uitați acest PIN, veți fi deconectat din aplica "Vă rugăm să introduceți același cod PIN de două ori" "Codurile PIN nu corespund" "Va trebui să vă reconectați și să creați un cod PIN nou pentru a continua" - "Acest device este în curs de eliminare" + "Sunteți deconectat" "Aveți %1$d încercare de deblocare" "Aveți %1$d încercări de deblocare" 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 14687f9272..0cfb2e88cd 100644 --- a/features/lockscreen/impl/src/main/res/values-sk/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-sk/translations.xml @@ -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" - "Odoberanie zariadenia…" + "Prebieha odhlasovanie…" 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 25e96003c8..5c19889282 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-vi/translations.xml b/features/lockscreen/impl/src/main/res/values-vi/translations.xml index 59dd89b114..2a177b843b 100644 --- a/features/lockscreen/impl/src/main/res/values-vi/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-vi/translations.xml @@ -3,7 +3,6 @@ "xác thực sinh trắc học" "mở khóa sinh trắc học" "Mở khóa bằng sinh trắc học" - "Xác nhận sinh trắc học" "Quên mã PIN rồi à?" "Thay đổi mã PIN" "Cho phép mở khóa bằng sinh trắc học" diff --git a/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml b/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml index 2bd329aea4..799db8f84c 100644 --- a/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml @@ -23,7 +23,7 @@ "請輸入相同的 PIN 碼兩次" "PIN 碼不一樣" "您需要重新登入並建立新的 PIN 碼才能繼續" - "此裝置已被移除" + "您即將登出" "您有 %1$d 次解鎖的機會" @@ -32,5 +32,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 f3e93668fd..defe7a0e32 100644 --- a/features/lockscreen/impl/src/main/res/values-zh/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-zh/translations.xml @@ -8,22 +8,22 @@ "更改 PIN 码" "允许生物识别解锁" "移除 PIN 码" - "你确定要删除 PIN 码?" + "您确定要删除 PIN 码吗?" "移除 PIN 码?" "允许 %1$s" "我宁愿使用 PIN 码" "节省时间,用 %1$s 来解锁应用程序" "选择 PIN 码" "确认 PIN 码" - "锁定 %1$s 以增加聊天的安全性。 + "锁定 %1$s 以为聊天增加安全性。 -选择好记的 PIN 码。如果忘掉了此 PIN 码,你将被迫从 app 注销。" - "出于安全考虑,你不能使用此 PIN 码" +选择好记的 PIN 码。如果忘掉了这个 PIN 码,就不得不登出应用。" + "出于安全原因,您不能选择这个 PIN 码" "选择不同的 PIN 码" "请输入两次相同的 PIN 码" "PIN 码不匹配" - "你需要重新登录并创建新的 PIN 码才能继续" - "正在被移除该设备" + "您需要重新登录并创建新的 PIN 才能继续" + "您正在登出" "还剩 %1$d 次解锁机会" @@ -32,5 +32,5 @@ "使用生物识别" "使用 PIN 码" - "正在移除设备…" + "正在删除设备……" 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 f906d0d6ba..9082f20a55 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,12 +14,9 @@ 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 @@ -41,18 +38,18 @@ class DefaultLockScreenServiceTest { @Test fun `when the pin is mandatory, isSetupRequired emits true`() = runTest { - val secretKeyRepository = SimpleSecretKeyRepository() + val lockScreenStore = InMemoryLockScreenStore() val sut = createDefaultLockScreenService( lockScreenConfig = aLockScreenConfig(isPinMandatory = true), - secretKeyRepository = secretKeyRepository, + lockScreenStore = lockScreenStore, ) sut.isSetupRequired().test { assertThat(awaitItem()).isTrue() // When the user configures the pin code, the setup is not required anymore - secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, true) + lockScreenStore.saveEncryptedPinCode("encryptedCode") assertThat(awaitItem()).isFalse() // Users deletes the pin code - secretKeyRepository.deleteKey("elementx.SECRET_KEY_ALIAS_PIN_CODE") + lockScreenStore.deleteEncryptedPinCode() assertThat(awaitItem()).isTrue() } } @@ -60,16 +57,16 @@ class DefaultLockScreenServiceTest { @Test fun `when the last session is deleted, the pin code is removed`() = runTest { val sessionObserver = FakeSessionObserver() - val secretKeyRepository = SimpleSecretKeyRepository() + val lockScreenStore = InMemoryLockScreenStore() val sut = createDefaultLockScreenService( lockScreenConfig = aLockScreenConfig(isPinMandatory = true), - secretKeyRepository = secretKeyRepository, + lockScreenStore = lockScreenStore, sessionObserver = sessionObserver, ) sut.isPinSetup().test { assertThat(awaitItem()).isFalse() // When the user configure the pin code, the setup is not required anymore - secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, true) + lockScreenStore.saveEncryptedPinCode("encryptedCode") assertThat(awaitItem()).isTrue() sessionObserver.onSessionDeleted("userId", wasLastSession = false) expectNoEvents() @@ -82,10 +79,8 @@ 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 63729f941a..073bdc799d 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 @@ -12,6 +12,6 @@ class FakeBiometricAuthenticator( override val isActive: Boolean = false, private val authenticateLambda: suspend () -> BiometricAuthenticator.AuthenticationResult = { BiometricAuthenticator.AuthenticationResult.Success }, ) : BiometricAuthenticator { - override suspend fun setup() = Unit + override fun setup() = Unit override suspend fun authenticate() = 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 0ae8552334..9e9b892582 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 @@ -15,7 +15,6 @@ class FakeBiometricAuthenticatorManager( override var isDeviceSecured: Boolean = true, override var hasAvailableAuthenticator: Boolean = false, private val createBiometricAuthenticator: () -> BiometricAuthenticator = { FakeBiometricAuthenticator() }, - private val disableLambda: suspend () -> Unit = { }, ) : BiometricAuthenticatorManager { override fun addCallback(callback: BiometricAuthenticator.Callback) { // no-op @@ -38,8 +37,4 @@ class FakeBiometricAuthenticatorManager( createBiometricAuthenticator() } } - - override suspend fun disable() { - disableLambda() - } } 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 312a33b7f1..61acf71cdd 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,7 +15,12 @@ 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) @@ -43,6 +48,10 @@ 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 85eb4a37e7..ef3e94f27f 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(LockScreenSettingsEvent.OnRemovePin) + state.eventSink(LockScreenSettingsEvents.OnRemovePin) } awaitLastSequentialItem().also { state -> assertThat(state.showRemovePinConfirmation).isTrue() - state.eventSink(LockScreenSettingsEvent.CancelRemovePin) + state.eventSink(LockScreenSettingsEvents.CancelRemovePin) } awaitLastSequentialItem().also { state -> assertThat(state.showRemovePinConfirmation).isFalse() - state.eventSink(LockScreenSettingsEvent.OnRemovePin) + state.eventSink(LockScreenSettingsEvents.OnRemovePin) } awaitLastSequentialItem().also { state -> assertThat(state.showRemovePinConfirmation).isTrue() - state.eventSink(LockScreenSettingsEvent.ConfirmRemovePin) + state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin) } consumeItemsUntilPredicate { it.showRemovePinOption.not() @@ -93,7 +93,7 @@ class LockScreenSettingsPresenterTest { presenter.test { skipItems(1) awaitItem().also { state -> - state.eventSink(LockScreenSettingsEvent.ToggleBiometricAllowed) + state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed) } awaitItem().also { state -> assertThat(state.isBiometricEnabled).isTrue() @@ -114,7 +114,7 @@ class LockScreenSettingsPresenterTest { presenter.test { skipItems(1) awaitItem().also { state -> - state.eventSink(LockScreenSettingsEvent.ToggleBiometricAllowed) + state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed) } } } @@ -137,7 +137,7 @@ class LockScreenSettingsPresenterTest { skipItems(1) awaitItem().also { state -> assertThat(state.isBiometricEnabled).isTrue() - state.eventSink(LockScreenSettingsEvent.ToggleBiometricAllowed) + state.eventSink(LockScreenSettingsEvents.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 9dde220906..3f87c1dccf 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,6 +8,9 @@ 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 @@ -15,7 +18,6 @@ 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 @@ -28,10 +30,12 @@ class SetupBiometricPresenterTest { FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Success }) }) val presenter = createSetupBiometricPresenter(lockScreenStore, fakeBiometricAuthenticatorManager) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { awaitItem().also { state -> assertThat(state.isBiometricSetupDone).isFalse() - state.eventSink(SetupBiometricEvent.AllowBiometric) + state.eventSink(SetupBiometricEvents.AllowBiometric) } awaitItem().also { state -> assertThat(state.isBiometricSetupDone).isTrue() @@ -47,10 +51,12 @@ class SetupBiometricPresenterTest { FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Failure() }) }) val presenter = createSetupBiometricPresenter(lockScreenStore, fakeBiometricAuthenticatorManager) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { awaitItem().also { state -> assertThat(state.isBiometricSetupDone).isFalse() - state.eventSink(SetupBiometricEvent.AllowBiometric) + state.eventSink(SetupBiometricEvents.AllowBiometric) } } assertThat(lockScreenStore.isBiometricUnlockAllowed().first()).isFalse() @@ -60,10 +66,12 @@ class SetupBiometricPresenterTest { fun `present - skip flow`() = runTest { val lockScreenStore = InMemoryLockScreenStore() val presenter = createSetupBiometricPresenter(lockScreenStore) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { awaitItem().also { state -> assertThat(state.isBiometricSetupDone).isFalse() - state.eventSink(SetupBiometricEvent.UsePin) + state.eventSink(SetupBiometricEvents.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 9d63f9e26b..6a1d32e879 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,6 +8,9 @@ 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 @@ -21,7 +24,6 @@ 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 @@ -41,7 +43,9 @@ class SetupPinPresenterTest { } } val presenter = createSetupPinPresenter(callback) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { awaitItem().also { state -> state.choosePinEntry.assertEmpty() state.confirmPinEntry.assertEmpty() @@ -59,7 +63,7 @@ class SetupPinPresenterTest { awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(forbiddenPin) assertThat(state.setupPinFailure).isEqualTo(SetupPinFailure.ForbiddenPin) - state.eventSink(SetupPinEvent.ClearFailure) + state.eventSink(SetupPinEvents.ClearFailure) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertEmpty() @@ -78,7 +82,7 @@ class SetupPinPresenterTest { state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertText(mismatchedPin) assertThat(state.setupPinFailure).isEqualTo(SetupPinFailure.PinsDoNotMatch) - state.eventSink(SetupPinEvent.ClearFailure) + state.eventSink(SetupPinEvents.ClearFailure) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertEmpty() @@ -104,7 +108,7 @@ class SetupPinPresenterTest { } private fun SetupPinState.onPinEntryChanged(pinEntry: String) { - eventSink(SetupPinEvent.OnPinEntryChanged(pinEntry, isConfirmationStep)) + eventSink(SetupPinEvents.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 fa7d05b5cb..f5bfb11818 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,6 +8,9 @@ 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 @@ -16,14 +19,12 @@ 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 @@ -35,7 +36,9 @@ class PinUnlockPresenterTest { @Test fun `present - success verify flow`() = runTest { val presenter = createPinUnlockPresenter() - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { awaitItem().also { state -> assertThat(state.pinEntry).isInstanceOf(AsyncData.Uninitialized::class.java) assertThat(state.showWrongPinTitle).isFalse() @@ -47,17 +50,17 @@ class PinUnlockPresenterTest { awaitItem().also { state -> assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Success::class.java) - state.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('1'))) - state.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('2'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) } skipItems(1) awaitItem().also { state -> state.pinEntry.assertText(halfCompletePin) - 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'))) + 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'))) } skipItems(4) awaitItem().also { state -> @@ -70,7 +73,9 @@ class PinUnlockPresenterTest { @Test fun `present - failure verify flow`() = runTest { val presenter = createPinUnlockPresenter() - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { skipItems(1) val initialState = awaitItem().also { state -> assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) @@ -78,10 +83,10 @@ class PinUnlockPresenterTest { } val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0 repeat(numberOfAttempts) { - 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'))) + 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'))) } skipItems(4 * numberOfAttempts + 2) awaitItem().also { state -> @@ -97,25 +102,27 @@ class PinUnlockPresenterTest { val signOutLambda = lambdaRecorder {} val signOut = FakeLogoutUseCase(signOutLambda) val presenter = createPinUnlockPresenter(logoutUseCase = signOut) - presenter.test { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { skipItems(1) awaitItem().also { state -> assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Success::class.java) - state.eventSink(PinUnlockEvent.OnForgetPin) + state.eventSink(PinUnlockEvents.OnForgetPin) } awaitItem().also { state -> assertThat(state.showSignOutPrompt).isTrue() assertThat(state.isSignOutPromptCancellable).isTrue() - state.eventSink(PinUnlockEvent.ClearSignOutPrompt) + state.eventSink(PinUnlockEvents.ClearSignOutPrompt) } awaitItem().also { state -> assertThat(state.showSignOutPrompt).isFalse() - state.eventSink(PinUnlockEvent.OnForgetPin) + state.eventSink(PinUnlockEvents.OnForgetPin) } awaitItem().also { state -> assertThat(state.showSignOutPrompt).isTrue() - state.eventSink(PinUnlockEvent.SignOut) + state.eventSink(PinUnlockEvents.SignOut) } skipItems(2) awaitItem().also { state -> @@ -125,28 +132,6 @@ 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) - } - } - } - private fun AsyncData.assertText(text: String) { dataOrNull()?.assertText(text) } @@ -154,10 +139,9 @@ class PinUnlockPresenterTest { private suspend fun TestScope.createPinUnlockPresenter( biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(), callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(), - logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = {}), - pinCodeManager: PinCodeManager = aPinCodeManager() + logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }), ): PinUnlockPresenter { - pinCodeManager.apply { + val pinCodeManager = aPinCodeManager().apply { addCallback(callback) createPinCode(completePin) } 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 e6d1659778..1ecb79bd67 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,57 +6,60 @@ * 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 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() + @Test - fun `clicking on a number emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on a number emits the expected event`() { val eventsRecorder = EventsRecorder() - setPinKeyPad(onClick = eventsRecorder) - onNode(hasText("1")).performClick() + rule.setPinKeyPad(onClick = eventsRecorder) + rule.onNode(hasText("1")).performClick() eventsRecorder.assertSingle(PinKeypadModel.Number('1')) } @Test - fun `clicking on the delete previous character button emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on the delete previous character button emits the expected event`() { val eventsRecorder = EventsRecorder() - setPinKeyPad(onClick = eventsRecorder) - onNode(hasContentDescription(activity!!.getString(CommonStrings.a11y_delete))).performClick() + rule.setPinKeyPad(onClick = eventsRecorder) + rule.onNode(hasContentDescription(rule.activity.getString(CommonStrings.a11y_delete))).performClick() eventsRecorder.assertSingle(PinKeypadModel.Back) } @OptIn(ExperimentalTestApi::class) @Test - fun `typing using the hardware keyboard emits the expected events`() = runAndroidComposeUiTest { + fun `typing using the hardware keyboard emits the expected events`() { val eventsRecorder = EventsRecorder() - setPinKeyPad(onClick = eventsRecorder) - onNodeWithText("1").requestFocus() - onAllNodes(isRoot())[0].performKeyInput { + rule.setPinKeyPad(onClick = eventsRecorder) + rule.onNodeWithText("1").requestFocus() + rule.onAllNodes(isRoot())[0].performKeyInput { val keys = listOf( Key.A, Key.NumPad1, @@ -115,7 +118,7 @@ class PinKeypadTest { ) } - private fun AndroidComposeUiTest.setPinKeyPad( + private fun AndroidComposeTestRule.setPinKeyPad( onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 86a8e0e7bc..12af922cbe 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -60,6 +60,7 @@ 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) @@ -68,7 +69,7 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.qrcode) - implementation(projects.libraries.oauth.api) + implementation(projects.libraries.oidc.api) implementation(projects.libraries.uiUtils) implementation(projects.libraries.wellknown.api) implementation(libs.androidx.browser) @@ -80,8 +81,9 @@ 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.oauth.test) + testImplementation(projects.libraries.oidc.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 978d28dfa3..fb384d505a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -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.OAuthDetails -import io.element.android.libraries.oauth.api.OAuthAction -import io.element.android.libraries.oauth.api.OAuthActionFlow +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 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 oAuthActionFlow: OAuthActionFlow, + private val oidcActionFlow: OidcActionFlow, @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) - oAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true)) + oidcActionFlow.post(OidcAction.GoBack(toUnblock = true)) } } } @@ -161,8 +161,8 @@ class LoginFlowNode( backstack.push(NavTarget.LoginPassword()) } - override fun navigateToOAuth(oAuthDetails: OAuthDetails) { - navigateToMas(oAuthDetails) + override fun navigateToOidc(oidcDetails: OidcDetails) { + navigateToMas(oidcDetails) } override fun navigateToCreateAccount(url: String) { @@ -197,8 +197,8 @@ class LoginFlowNode( callback.navigateToBugReport() } - override fun navigateToOAuth(oAuthDetails: OAuthDetails) { - navigateToMas(oAuthDetails) + override fun navigateToOidc(oidcDetails: OidcDetails) { + navigateToMas(oidcDetails) } override fun navigateToCreateAccount(url: String) { @@ -243,8 +243,8 @@ class LoginFlowNode( } NavTarget.ChooseAccountProvider -> { val callback = object : ChooseAccountProviderNode.Callback { - override fun navigateToOAuth(oAuthDetails: OAuthDetails) { - navigateToMas(oAuthDetails) + override fun navigateToOidc(oidcDetails: OidcDetails) { + navigateToMas(oidcDetails) } override fun navigateToCreateAccount(url: String) { @@ -270,8 +270,8 @@ class LoginFlowNode( isAccountCreation = navTarget.isAccountCreation, ) val callback = object : ConfirmAccountProviderNode.Callback { - override fun navigateToOAuth(oAuthDetails: OAuthDetails) { - navigateToMas(oAuthDetails) + override fun navigateToOidc(oidcDetails: OidcDetails) { + navigateToMas(oidcDetails) } override fun navigateToCreateAccount(url: String) { @@ -333,10 +333,10 @@ class LoginFlowNode( } } - private fun navigateToMas(oAuthDetails: OAuthDetails) { + private fun navigateToMas(oidcDetails: OidcDetails) { activity?.let { externalAppStarted = true - it.openUrlInChromeCustomTab(null, darkTheme, oAuthDetails.url) + it.openUrlInChromeCustomTab(null, darkTheme, oidcDetails.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 dfddd1d496..c928c05239 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,6 +28,8 @@ 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 @@ -69,6 +71,7 @@ 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 @@ -116,6 +119,10 @@ 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. @@ -151,6 +158,11 @@ 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 560e6123c1..2f4af14237 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.OAuth -> Error(messageStr = error.message) + is AuthenticationException.Oidc -> 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 3c871a8a1d..78be770bfc 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt @@ -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.OAuthPrompt -import io.element.android.libraries.oauth.api.OAuthAction -import io.element.android.libraries.oauth.api.OAuthActionFlow +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 /** * This class is responsible for managing the login flow, including handling OIDC actions and @@ -35,7 +35,7 @@ import io.element.android.libraries.oauth.api.OAuthActionFlow */ @Inject class LoginHelper( - private val oAuthActionFlow: OAuthActionFlow, + private val oidcActionFlow: OidcActionFlow, private val authenticationService: MatrixAuthenticationService, private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, ) { @@ -44,9 +44,9 @@ class LoginHelper( @Composable fun collectLoginMode(): State> { LaunchedEffect(Unit) { - oAuthActionFlow.collect { oAuthAction -> - if (oAuthAction != null) { - onOAuthAction(oAuthAction) + oidcActionFlow.collect { oidcAction -> + if (oidcAction != null) { + onOidcAction(oidcAction) } } } @@ -73,11 +73,11 @@ class LoginHelper( throw it } }.map { matrixHomeServerDetails -> - if (matrixHomeServerDetails.supportsOAuthLogin) { + if (matrixHomeServerDetails.supportsOidcLogin) { // Retrieve the details right now - val oAuthPrompt = if (isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login - LoginMode.OAuth( - authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = loginHint).getOrThrow() + val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login + LoginMode.Oidc( + authenticationService.getOidcUrl(prompt = oidcPrompt, loginHint = loginHint).getOrThrow() ) } else if (isAccountCreation) { val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) @@ -99,16 +99,16 @@ class LoginHelper( ) } - private suspend fun onOAuthAction(oAuthAction: OAuthAction) { - if (oAuthAction is OAuthAction.GoBack && oAuthAction.toUnblock && loginModeState.value !is AsyncData.Loading) { + private suspend fun onOidcAction(oidcAction: OidcAction) { + if (oidcAction is OidcAction.GoBack && oidcAction.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 (oAuthAction) { - is OAuthAction.GoBack -> { - authenticationService.cancelOAuthLogin() + when (oidcAction) { + is OidcAction.GoBack -> { + authenticationService.cancelOidcLogin() .onSuccess { loginModeState.value = AsyncData.Uninitialized } @@ -116,13 +116,13 @@ class LoginHelper( loginModeState.value = AsyncData.Failure(failure) } } - is OAuthAction.Success -> { - authenticationService.loginWithOAuth(oAuthAction.url) + is OidcAction.Success -> { + authenticationService.loginWithOidc(oidcAction.url) .onFailure { failure -> loginModeState.value = AsyncData.Failure(failure) } } } - oAuthActionFlow.reset() + oidcActionFlow.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 5ea52e0ebd..08e604ef20 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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails sealed interface LoginMode { data object PasswordLogin : LoginMode - data class OAuth(val oAuthDetails: OAuthDetails) : LoginMode + data class Oidc(val oidcDetails: OidcDetails) : 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 3549e17457..f88e34bf4a 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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -32,7 +32,7 @@ fun LoginModeView( loginMode: AsyncData, onClearError: () -> Unit, onLearnMoreClick: () -> Unit, - onOAuthDetails: (OAuthDetails) -> Unit, + onOidcDetails: (OidcDetails) -> 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.OAuth -> onOAuthDetails(loginModeData.oAuthDetails) + is LoginMode.Oidc -> onOidcDetails(loginModeData.oidcDetails) LoginMode.PasswordLogin -> onNeedLoginPassword() is LoginMode.AccountCreation -> onCreateAccountContinue(loginModeData.url) } @@ -137,7 +137,7 @@ internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorProvider:: loginMode = AsyncData.Failure(error), onClearError = {}, onLearnMoreClick = {}, - onOAuthDetails = {}, + onOidcDetails = {}, 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 03264551a5..613aa6aeb6 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.OAuthMetadataInvalid -> { - Timber.e(error, "OAuth metadata is invalid") + is QrLoginException.OidcMetadataInvalid -> { + Timber.e(error, "OIDC 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 5f79f197d9..5dc6ebbd6b 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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails @ContributesNode(AppScope::class) @AssistedInject @@ -31,7 +31,7 @@ class ChooseAccountProviderNode( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun navigateToLoginPassword() - fun navigateToOAuth(oAuthDetails: OAuthDetails) + fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToCreateAccount(url: String) } @@ -45,7 +45,7 @@ class ChooseAccountProviderNode( state = state, modifier = modifier, onBackClick = ::navigateUp, - onOAuthDetails = callback::navigateToOAuth, + onOidcDetails = callback::navigateToOidc, onNeedLoginPassword = callback::navigateToLoginPassword, onLearnMoreClick = { openLearnMorePage(context) }, onCreateAccountContinue = callback::navigateToCreateAccount, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt index f05606dbc3..cdb80304a7 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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.ui.strings.CommonStrings @Composable fun ChooseAccountProviderView( state: ChooseAccountProviderState, onBackClick: () -> Unit, - onOAuthDetails: (OAuthDetails) -> Unit, + onOidcDetails: (OidcDetails) -> Unit, onNeedLoginPassword: () -> Unit, onLearnMoreClick: () -> Unit, onCreateAccountContinue: (url: String) -> Unit, @@ -129,7 +129,7 @@ fun ChooseAccountProviderView( state.eventSink(ChooseAccountProviderEvents.ClearError) }, onLearnMoreClick = onLearnMoreClick, - onOAuthDetails = onOAuthDetails, + onOidcDetails = onOidcDetails, onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) @@ -144,7 +144,7 @@ internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountPro state = state, onBackClick = { }, onLearnMoreClick = { }, - onOAuthDetails = { }, + onOidcDetails = { }, 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 cfbd86f363..f2ff998652 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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.UserId import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -54,7 +54,7 @@ class ClassicFlowNode( interface Callback : Plugin { fun navigateToOnBoarding(allowBackNavigation: Boolean) fun navigateToLoginPassword() - fun navigateToOAuth(oAuthDetails: OAuthDetails) + fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToCreateAccount(url: String) } @@ -111,8 +111,8 @@ class ClassicFlowNode( callback.navigateToLoginPassword() } - override fun navigateToOAuth(oAuthDetails: OAuthDetails) { - callback.navigateToOAuth(oAuthDetails) + override fun navigateToOidc(oidcDetails: OidcDetails) { + callback.navigateToOidc(oidcDetails) } 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 d5acca38ae..c42248a3f8 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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails 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 navigateToOAuth(oAuthDetails: OAuthDetails) + fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToCreateAccount(url: String) fun navigateToMissingKeyBackup() } @@ -60,7 +60,7 @@ class LoginWithClassicNode( state = state, modifier = modifier, onOtherOptionsClick = callback::navigateToOtherOptions, - onOAuthDetails = callback::navigateToOAuth, + onOidcDetails = callback::navigateToOidc, onNeedLoginPassword = callback::navigateToLoginPassword, onLearnMoreClick = { openLearnMorePage(context) }, onCreateAccountContinue = callback::navigateToCreateAccount, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt index 31b1770f63..d8dcfeb072 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,14 +12,13 @@ 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 = USER_NAME_ALICE), + aLoginWithClassicState(isElementPro = true, displayName = "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 b1ca50fe61..6b5c48f1ec 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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -59,7 +59,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun LoginWithClassicView( state: LoginWithClassicState, onOtherOptionsClick: () -> Unit, - onOAuthDetails: (OAuthDetails) -> Unit, + onOidcDetails: (OidcDetails) -> Unit, onNeedLoginPassword: () -> Unit, onLearnMoreClick: () -> Unit, onCreateAccountContinue: (url: String) -> Unit, @@ -200,7 +200,7 @@ fun LoginWithClassicView( state.eventSink(LoginWithClassicEvent.ClearError) }, onLearnMoreClick = onLearnMoreClick, - onOAuthDetails = onOAuthDetails, + onOidcDetails = onOidcDetails, onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) @@ -212,7 +212,7 @@ internal fun LoginWithClassicViewPreview(@PreviewParameter(LoginWithClassicState LoginWithClassicView( state = state, onOtherOptionsClick = {}, - onOAuthDetails = {}, + onOidcDetails = {}, 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 928a493dc1..e3643afbf2 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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails @ContributesNode(AppScope::class) @AssistedInject @@ -44,7 +44,7 @@ class ConfirmAccountProviderNode( interface Callback : Plugin { fun navigateToLoginPassword() - fun navigateToOAuth(oAuthDetails: OAuthDetails) + fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToCreateAccount(url: String) fun navigateToChangeAccountProvider() } @@ -58,7 +58,7 @@ class ConfirmAccountProviderNode( ConfirmAccountProviderView( state = state, modifier = modifier, - onOAuthDetails = callback::navigateToOAuth, + onOidcDetails = callback::navigateToOidc, 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 c2525f3756..a175ab556d 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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -38,7 +38,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun ConfirmAccountProviderView( state: ConfirmAccountProviderState, - onOAuthDetails: (OAuthDetails) -> Unit, + onOidcDetails: (OidcDetails) -> Unit, onNeedLoginPassword: () -> Unit, onLearnMoreClick: () -> Unit, onCreateAccountContinue: (url: String) -> Unit, @@ -103,7 +103,7 @@ fun ConfirmAccountProviderView( eventSink(ConfirmAccountProviderEvents.ClearError) }, onLearnMoreClick = onLearnMoreClick, - onOAuthDetails = onOAuthDetails, + onOidcDetails = onOidcDetails, onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) @@ -117,7 +117,7 @@ internal fun ConfirmAccountProviderViewPreview( ) = ElementPreview { ConfirmAccountProviderView( state = state, - onOAuthDetails = {}, + onOidcDetails = {}, onNeedLoginPassword = {}, onCreateAccountContinue = {}, onLearnMoreClick = {}, 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 99f7e86fd3..5572c412a0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt @@ -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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails @ContributesNode(AppScope::class) @AssistedInject @@ -40,7 +40,7 @@ class OnBoardingNode( fun navigateToQrCode() fun navigateToBugReport() fun navigateToLoginPassword() - fun navigateToOAuth(oAuthDetails: OAuthDetails) + fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToCreateAccount(url: String) fun navigateToDeveloperSettings() fun onDone() @@ -71,7 +71,7 @@ class OnBoardingNode( onCreateAccount = callback::navigateToSignUpFlow, onSignInWithQrCode = callback::navigateToQrCode, onReportProblem = callback::navigateToBugReport, - onOAuthDetails = callback::navigateToOAuth, + onOidcDetails = callback::navigateToOidc, onNeedLoginPassword = callback::navigateToLoginPassword, onLearnMoreClick = { openLearnMorePage(context) }, onCreateAccountContinue = callback::navigateToCreateAccount, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index 53c36ac4f8..5ee7ab6ac4 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -68,7 +68,7 @@ fun OnBoardingView( onSignInWithQrCode: () -> Unit, onSignIn: (mustChooseAccountProvider: Boolean) -> Unit, onCreateAccount: () -> Unit, - onOAuthDetails: (OAuthDetails) -> Unit, + onOidcDetails: (OidcDetails) -> Unit, onNeedLoginPassword: () -> Unit, onLearnMoreClick: () -> Unit, onCreateAccountContinue: (url: String) -> Unit, @@ -82,7 +82,7 @@ fun OnBoardingView( state.eventSink(OnBoardingEvents.ClearError) }, onLearnMoreClick = onLearnMoreClick, - onOAuthDetails = onOAuthDetails, + onOidcDetails = onOidcDetails, onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) @@ -354,7 +354,7 @@ internal fun OnBoardingViewPreview( onSignIn = {}, onCreateAccount = {}, onReportProblem = {}, - onOAuthDetails = {}, + onOidcDetails = {}, onNeedLoginPassword = {}, onLearnMoreClick = {}, onCreateAccountContinue = {}, 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 d9d69503c5..307ccf7263 100644 --- a/features/login/impl/src/main/res/values-be/translations.xml +++ b/features/login/impl/src/main/res/values-be/translations.xml @@ -24,7 +24,7 @@ "Няправільнае імя карыстальніка і/або пароль" "Гэта несапраўдны ідэнтыфікатар карыстальніка. Чаканы фармат: ‘@user:homeserver.org’" "Гэты сервер настроены на выкарыстанне маркераў абнаўлення. Яны не падтрымліваюцца пры ўваходзе на аснове пароля." - "Выбраны хатні сервер не падтрымлівае пароль або ўваход у OAuth. Калі ласка, звярніцеся да адміністратара або абярыце іншы хатні сервер." + "Выбраны хатні сервер не падтрымлівае пароль або ўваход у OIDC. Калі ласка, звярніцеся да адміністратара або абярыце іншы хатні сервер." "Увядзіце свае даныя" "Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі." "Сардэчна запрашаем!" 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 c6c74ff1a8..6ccc7c9129 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’" - "Избраният сървър не поддържа влизане с парола или OAuth. Моля, свържете се с вашия администратор или изберете друг сървър." + "Избраният сървър не поддържа влизане с парола или OIDC. Моля, свържете се с вашия администратор или изберете друг сървър." "Въведете своите данни" "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 deleted file mode 100644 index f079e7b071..0000000000 --- a/features/login/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - "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 3181e823bb..73f0dd51cc 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -28,29 +28,20 @@ "Jaká je adresa vašeho serveru?" "Vyberte váš server" "Vytvořit účet" - "Tento účet byl smazán." + "Tento účet byl deaktivová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 OAuth. Kontaktujte prosím svého správce nebo vyberte jiný domovský server." + "Vybraný domovský server nepodporuje přihlášení pomocí hesla nebo OIDC. 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!" "Přihlaste se k %1$s" - "Otevřít Element Classic" - "Otevřete Element Classic na svém zařízení" - "Přejděte do Nastavení > Zabezpečení a soukromí" - "V části Správa kryptografických klíčů vyberte Obnova šifrovaných zpráv" - "Postupujte podle pokynů k povolení úložiště klíčů" - "Vraťte se do %1$s" - "Povolte úložiště klíčů, než budete pokračovat na %1$s" "Verze %1$s" - "Kontrola účtu" "Ruční přihlášení" "Přihlaste se k %1$s" "Přihlásit se pomocí QR kódu" "Vytvořit účet" - "Vítejte zpět" "Vítejte v dosud nejrychlejším %1$su. Vylepšený pro rychlost a jednoduchost." "Vítejte v %1$su. Vylepšený, pro rychlost a jednoduchost." "Buďte ve svém živlu" 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 0f44287ed4..b8988a9889 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 OAuth. Cysylltwch â\'ch gweinyddwr neu dewis gweinydd cartref arall." + "Nid yw\'r gweinydd cartref ddewiswyd yn cefnogi cyfrinair na mewngofnodi OIDC. 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 35d66a6e69..2b2f00267b 100644 --- a/features/login/impl/src/main/res/values-da/translations.xml +++ b/features/login/impl/src/main/res/values-da/translations.xml @@ -28,29 +28,20 @@ "Hvad er adressen på din server?" "Vælg din server" "Opret konto" - "Denne konto er blevet slettet." + "Denne konto er blevet deaktiveret." "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 OAuth-login. Kontakt venligst din administrator eller vælg en anden hjemmeserver." + "Den valgte hjemmeserver understøtter ikke adgangskode eller OIDC-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" "Opret konto" - "Velkommen tilbage" "Velkommen til den hurtigste %1$s nogensinde. Supercharged til hastighed og enkelhed." "Velkommen til %1$s. Ladet med hastighed og enkelhed." "Vær i dit rette Element" 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 89e4412d0f..f1d426e134 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -5,9 +5,9 @@ "Gib einen Suchbegriff oder eine Domainadresse ein." "Suche nach einem Unternehmen, einer Community oder einem privaten Server." "Kontoanbieter finden" - "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würdest." + "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden." "Du bist dabei, dich bei %s anzumelden" - "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würdest." + "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden." "Du bist dabei, ein Konto bei %s zu erstellen" "Matrix.org ist ein großer, kostenloser Server im öffentlichen Matrix-Netzwerk für eine sichere, dezentralisierte Kommunikation, der von der Matrix.org Foundation betrieben wird." "Sonstige" @@ -28,11 +28,11 @@ "Wie lautet die Adresse deines Servers?" "Wähle deinen Server aus" "Konto erstellen" - "Dieses Konto wurde gelöscht." + "Dieses Konto wurde deaktiviert." "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 OAuth. Bitte kontaktiere deinen Administrator oder wähle einen anderen Homeserver." + "Der ausgewählte Homeserver unterstützt weder den Login per Passwort noch per OIDC. 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!" @@ -42,7 +42,6 @@ "Anmelden bei %1$s" "Mit QR-Code anmelden" "Konto erstellen" - "Willkommen zurück" "Willkommen beim schnellsten %1$s aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit." "Willkommen zu %1$s. Aufgeladen, für Geschwindigkeit und Einfachheit." "Sei in Deinem Element" @@ -94,7 +93,7 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Ger "Kontoanbieter wechseln" "Ein privater Server für die Mitarbeiter von Element." "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." - "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würdest." + "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden." "Du bist dabei, dich bei %1$s anzumelden" "Kontoanbieter auswählen" "Du bist dabei, auf %1$s ein Konto zu erstellen" 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 045465902b..5f21a53c37 100644 --- a/features/login/impl/src/main/res/values-el/translations.xml +++ b/features/login/impl/src/main/res/values-el/translations.xml @@ -28,28 +28,20 @@ "Ποια είναι η διεύθυνση του διακομιστή σου;" "Επέλεξε το διακομιστή σου" "Δημιουργία λογαριασμού" - "Αυτός ο λογαριασμός έχει διαγραφεί." + "Αυτός ο λογαριασμός έχει απενεργοποιηθεί." "Λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης" "Αυτό δεν είναι έγκυρο αναγνωριστικό χρήστη. Αναμενόμενη μορφή: \'@χρήστης:homeserver.org\'" "Αυτός ο διακομιστής έχει ρυθμιστεί ώστε να χρησιμοποιεί διακριτικά ανανέωσης. Αυτά δεν υποστηρίζονται όταν χρησιμοποιείς σύνδεση μέσω κωδικού πρόσβασης." - "Ο επιλεγμένος οικιακός διακομιστής δεν υποστηρίζει κωδικό πρόσβασης ή σύνδεση OAuth. Επικοινωνήστε με τον διαχειριστή σου ή επέλεξε άλλο οικιακό διακομιστή." + "Ο επιλεγμένος οικιακός διακομιστής δεν υποστηρίζει κωδικό πρόσβασης ή σύνδεση OIDC. Επικοινωνήστε με τον διαχειριστή σου ή επέλεξε άλλο οικιακό διακομιστή." "Εισήγαγε τα στοιχεία σου" "Το 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-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml index 3b9ffbe2dd..df6a06ab29 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 OAuth. 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 OIDC. 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 dc37ed3e38..0a1a99383d 100644 --- a/features/login/impl/src/main/res/values-et/translations.xml +++ b/features/login/impl/src/main/res/values-et/translations.xml @@ -28,29 +28,20 @@ "Mis on sinu koduserveri aadress?" "Vali oma server" "Loo kasutajakonto" - "See kasutajakonto on kustutatud." + "Konto on kasutusest eemaldatud." "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 OAuth-põhist sisselogimist. Lisateavet saad koduserveri haldajalt, aga sa võid ka valida mõne teise serveri." + "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." "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" "Loo kasutajakonto" - "Tere tulemast tagasi" "Läbi aegade kiireim ja mugavaim %1$s." "Tere tulemast kasutama kiiret ja lihtsat suhtlusrakendust %1$s." "Ole oma elemendis" 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 78a13e5810..355e63546c 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 OAuth saio-hasierarekin. Jarri harremanetan administratzailearekin edo aukeratu beste zerbitzari bat." + "Hautatutako zerbitzaria ez da bateragarria pasahitz edo OIDC 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 c28c891c9a..ef7062a88a 100644 --- a/features/login/impl/src/main/res/values-fa/translations.xml +++ b/features/login/impl/src/main/res/values-fa/translations.xml @@ -15,17 +15,15 @@ "تغییر فراهم کنندهٔ حساب" "پلی گپگل" "ما نتوانستیم به این کارساز خانگی برسیم. لطفاً بررسی کنید که URL کارساز اصلی را به درستی وارد کرده اید. اگر URL صحیح است، برای کمک بیشتر با مدیر کارساز خانگی خود تماس بگیرید." - "سرور به دلیل مشکلی در فایل .well-known در دسترس نیست: %1$s" "نشانی کارساز خانگی" "ورود نشانی دامنه." "نشانی کارسازتان چیست؟" "کارسازتان را برگزینید" "ایجاد حساب" - "این حساب حذف شده است." + "این حساب از کار افتاده است." "نام کاربری یا گذرواژه نامعتبر است" "این یک شناسه کاربری معتبر نیست. قالب صحیح: ‪«@user:homeserver.or" - "این سرور برای استفاده از توکن‌های به‌روزرسانی پیکربندی شده است. این توکن‌ها هنگام استفاده از ورود مبتنی بر رمز عبور پشتیبانی نمی‌شوند." - "کارساز اصلی انتخاب شده از رمز عبور یا ورود OAuth پشتیبانی نمی کند. لطفا با مدیر خود تماس بگیرید یا یک کارساز خانگی دیگر را انتخاب کنید." + "کارساز اصلی انتخاب شده از رمز عبور یا ورود OIDC پشتیبانی نمی کند. لطفا با مدیر خود تماس بگیرید یا یک کارساز خانگی دیگر را انتخاب کنید." "جزییاتتان را وارد کنید" "ماتریکس شبکه‌ای بار برای ارتباطات نامتمرکز و امن است." "خوش برگشتید!" 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 8561bcfce3..af8e242309 100644 --- a/features/login/impl/src/main/res/values-fi/translations.xml +++ b/features/login/impl/src/main/res/values-fi/translations.xml @@ -28,29 +28,20 @@ "Mikä on palvelimesi osoite?" "Valitse palvelimesi" "Luo tili" - "Tämä tili on poistettu." + "Tämä tili on deaktivoitu." "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 OAuth-kirjautumista. Ota yhteyttä palvelimesi ylläpitäjään tai valitse toinen kotipalvelin." + "Valitsemasi kotipalvelin ei tue salasana- tai OIDC-kirjautumista. Ota yhteyttä palvelimesi ylläpitäjään tai valitse toinen kotipalvelin." "Anna tietosi" "Matrix on avoin verkko turvallista, hajautettua viestintää varten." "Tervetuloa takaisin!" "Kirjaudu sisään %1$s -palvelimelle" - "Avaa Element Classic" - "Avaa Element Classic laitteellasi" - "Mene kohtaan Asetukset > Tietoturva ja yksityisyys" - "Osiossa \"Salausavainten hallinta\", paina \"Salattujen viestien palautus\"." - "Noudata ohjeita" - "Palaa takaisin %1$s -sovellukseen" - "Ota avainten säilytys käyttöön ennen kuin jatkat %1$s -sovellukseen" "Versio %1$s" - "Tarkistetaan tiliä…" "Kirjaudu sisään manuaalisesti" "Kirjaudu sisään %1$s -palvelimelle" "Kirjaudu sisään QR-koodilla" "Luo tili" - "Tervetuloa takaisin" "Tervetuloa kaikkien aikojen nopeimpaan %1$s -sovellukseen. Ahdettu nopeudella ja yksinkertaisuudella." "Tervetuloa %1$s -sovellukseen. Ahdettu nopeudella ja yksinkertaisuudella." "Ole elementissäsi" 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 504517aad7..9846feec38 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -28,29 +28,20 @@ "Quelle est l’adresse de votre serveur ?" "Choisissez votre serveur" "Créer un compte" - "Ce compte a été supprimé." + "Ce compte a été désactivé." "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 OAuth. 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 OIDC. 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 !" "Connectez-vous à %1$s" - "Ouvrir Element Classic" - "Ouvrez Element Classic sur votre appareil" - "Aller à Paramètres > Sécurité et vie privée" - "Dans Gestion des clés cryptographiques, sélectionnez Récupération des messages chiffrés" - "Suivez les instructions pour activer votre stockage de clés" - "Revenez à %1$s" - "Activez le stockage de vos clés avant de continuer avec %1$s" "Version %1$s" - "Vérification du compte" "Se connecter manuellement" "Connectez-vous à %1$s" "Se connecter avec un code QR" "Créer un compte" - "Bon retour parmi nous" "Bienvenue dans l’application %1$s la plus rapide de tous les temps. Boosté pour plus de rapidité et de simplicité." "Bienvenue sur %1$s. Boosté, pour plus de rapidité et de simplicité." "Soyez dans votre Element" 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 f578790380..ced196f87e 100644 --- a/features/login/impl/src/main/res/values-hr/translations.xml +++ b/features/login/impl/src/main/res/values-hr/translations.xml @@ -32,25 +32,16 @@ "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 OAuth prijavu. Obratite se administratoru ili odaberite drugi matični poslužitelj." + "Odabrani matični poslužitelj ne podržava zaporku ili OIDC 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!" "Prijavi se na poslužitelj %1$s" - "Pokreni Element Classic" - "Otvorite Element Classic na svom uređaju" - "Idite na Postavke > Sigurnost i privatnost" - "šifrirane poruke" - "Slijedite upute za omogućavanje pohrane ključeva" - "Vrati se %1$s" - "Omogućite pohranu ključeva prije nego što nastavite na %1$s" "Inačica %1$s" - "Provjera računa…" "Prijavi se ručno" "Prijavi se na poslužitelj %1$s" "Prijavi se pomoću QR koda" "Izradi račun" - "Dobro došli natrag!" "Dobro došli u nikad brži %1$s. Snažniji no ikad za postizanje brzine i jednostavnosti." "Dobro došli u %1$s. Snažniji no ikad – za brzinu i jednostavnost." "Budi u elementu" 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 26534cc523..06014b77b7 100644 --- a/features/login/impl/src/main/res/values-hu/translations.xml +++ b/features/login/impl/src/main/res/values-hu/translations.xml @@ -28,29 +28,20 @@ "Mi a kiszolgálója címe?" "Válassza ki a kiszolgálóját" "Fiók létrehozása" - "Ez a fiók törölve lett." + "Ez a fiók deaktiválva 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 OAuth-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 OIDC-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!" "Bejelentkezés ide: %1$s" - "Nyissa meg az Element Classic alkalmazást" - "Nyissa meg az Element Classic alkalmazást az eszközén" - "Lépjen a Beállítások > Biztonság és adatvédelem menüponthoz" - "A Kriptográfiai kulcsok kezelése részben válassza a Titkosított üzenetek helyreállítása lehetőséget" - "Kövesse az utasításokat a kulcstároló engedélyezéséhez" - "Térjen vissza ide: %1$s" - "Engedélyezze a kulcstárolást a folytatás előtt ide: %1$s" "Verzió: %1$s" - "Fiók ellenőrzése" "Kézi bejelentkezés" "Bejelentkezés ide: %1$s" "Bejelentkezés QR-kóddal" "Fiók létrehozása" - "Üdvözöljük újra!" "Üdvözöljük a valaha volt leggyorsabb %1$sben. Felturbózva, a sebesség és az egyszerűség érdekében." "Üdvözli az %1$s. Felturbózva, a sebesség és az egyszerűség jegyében." "Legyen elemében" 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 4b9f3ffdef..e05fd8746d 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 OAuth. Silakan hubungi admin Anda atau pilih homeserver yang lain." + "Homeserver yang dipilih tidak mendukung log masuk kata sandi atau OIDC. 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 1f1b51cda8..0de1cc0690 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -28,29 +28,20 @@ "Qual è l\'indirizzo del tuo server?" "Seleziona il tuo server" "Crea account" - "Questo account è stato eliminato." + "Questo account è stato disattivato." "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 OAuth. Contatta il tuo amministratore o scegli un altro homeserver." + "L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver." "Inserisci i tuoi dati" "Matrix è una rete aperta per comunicazioni sicure e decentralizzate." "Bentornato!" "Accedi a %1$s" - "Apri Element Classic" - "Apri Element Classic sul tuo dispositivo" - "Vai su Impostazioni > Sicurezza & privacy" - "Nella gestione delle chiavi crittografiche, seleziona Recupero dei messaggi cifrati" - "Segui le istruzioni per abilitare l\'archiviazione delle chiavi" - "Torna a %1$s" - "Abilita l\'archivio delle chiavi prima di procedere con %1$s" "Versione %1$s" - "Verifica dell\'account" "Accedi manualmente" "Accedi a %1$s" "Accedi con codice QR" "Crea account" - "Bentornato" "Benvenuti nell\'%1$s più veloce di sempre. Potenziato per velocità e semplicità." "Benvenuto su %1$s. Potenziato in velocità e semplicità." "Sii nel tuo elemento" 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 42698e27dc..e2c89ba26e 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,29 +27,20 @@ "サーバーのアドレスは何ですか?" "サーバーを選択" "アカウントを作成" - "アカウントは削除されました。" + "このアカウントは無効化されています。" "ユーザー名またはパスワードが違います" "無効なユーザーIDです。正しい形式は \"@ユーザー:ホームサーバー\" です。" "このサーバーはリフレッシュトークンを使用します。パスワードを使用したログインとは併用できません。" - "指定したホームサーバはパスワードまたはOAuthによるログインに対応していません。管理者に問い合わせるか、異なるホームサーバーを使用してください。" + "指定したホームサーバはパスワードまたはOIDCによるログインに対応していません。管理者に問い合わせるか、異なるホームサーバーを使用してください。" "詳細を入力" "Matrix は安全で分散型のオープンなネットワークです。" "お待ちしておりました。" "%1$s にサインイン" - "Element Classic を開く" - "Element Classic をこの端末で開く" - "「設定- セキュリティとプライバシー」に移動します" - "暗号鍵の管理から、暗号化されたメッセージの回復を選択します" - "指示に従って、鍵の保管庫を有効化してください" - "%1$s に戻ってください" - "%1$s に続行する前に、鍵の保管庫を有効化してください" "バージョン %1$s" - "アカウントを確認中" "手動で指定してサインイン" "%1$s にサインイン" "QRコードでサインイン" "アカウントを作成" - "おかえりなさい" "最速の %1$s にようこそ。機能性と利便性を極限まで追求しました。" "機敏と利便を追求した %1$s へようこそ。" "Be in your element" @@ -80,8 +71,8 @@ "%1$s に非対応" "読み取る" "コンピュータで %1$s を開く" - "アバターをタップ" - "%1$s を選択" + "アバターをタップしてください" + "%1$s を選択してください" "\"新しい端末を追加\"" "この端末でQRコードを読み取る" "アカウント提供元が対応する場合にのみ使用できます。" @@ -98,11 +89,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 04db3fda13..54e277ec12 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’" "ეს სერვერი კონფიგურირებულია განახლების გასაღებების გამოსაყენებლად. პაროლზე დაფუძნებული შეცვლისას ისინი მხარდაჭერილი არაა." - "მოცემული სახლის სერვერი მხარს არ უჭერს პაროლით ან OAuth-ით შესვლას. გთხოვთ, დაუკავშირდეთ თქვენს ადმინისტრატორს ან აარჩიეთ სხვა სახლის სერვერი." + "მოცემული სახლის სერვერი მხარს არ უჭერს პაროლით ან OIDC-ით შესვლას. გთხოვთ, დაუკავშირდეთ თქვენს ადმინისტრატორს ან აარჩიეთ სხვა სახლის სერვერი." "შეიყვანეთ თქვენი დეტალები" "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 338ba628c9..69bf12cdb0 100644 --- a/features/login/impl/src/main/res/values-ko/translations.xml +++ b/features/login/impl/src/main/res/values-ko/translations.xml @@ -32,24 +32,16 @@ "잘못된 아이디/비밀번호" "이 사용자 ID는 유효하지 않습니다. 예상 형식: ‘@user:homeserver.org’" "이 서버는 새로 고침 토큰을 사용하도록 구성되어 있습니다. 비밀번호 기반 로그인을 사용하는 경우 이 기능은 지원되지 않습니다." - "선택한 홈 서버는 password 또는 OAuth 로그인을 지원하지 않습니다. 관리자에게 문의하거나 다른 홈 서버를 선택하세요." + "선택한 홈 서버는 password 또는 OIDC 로그인을 지원하지 않습니다. 관리자에게 문의하거나 다른 홈 서버를 선택하세요." "귀하의 세부 정보를 입력하십시오" "Matrix 는 안전하고 분산된 커뮤니케이션을 위한 개방형 네트워크입니다." "다시 돌아온 걸 환영합니다!" "%1$s 에 로그인합니다" - "Element Classic 열기" - "기기에서 Element Classic 앱을 열어 주세요" - "설정 > 보안 및 개인정보 보호로 이동하세요" - "암호화 키 관리에서 \'암호화된 메시지 복구\'를 선택하세요" - "안내에 따라 키 저장소를 활성화해 주세요" - "%1$s(으)로 돌아가기" - "%1$s(으)로 진행하기 전에 키 저장소를 활성화해 주세요." "버전 %1$s" "수동으로 로그인" "%1$s 에 로그인합니다" "QR 코드로 로그인" "계정 만들기" - "다시 오신 것을 환영합니다" "%1$s 에 오신 것을 환영합니다. 속도와 단순성을 극대화한 가장 빠른 버전입니다." "%1$s 에 오신 것을 환영합니다. 속도와 단순성을 위해 최적화된 앱입니다." "당신의 엘리먼트에 있어" diff --git a/features/login/impl/src/main/res/values-lt/translations.xml b/features/login/impl/src/main/res/values-lt/translations.xml index dc1a2b7ba6..f8f243f29d 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 OAuth prisijungimo. Susisiekite su serverio administracija arba pasirinkite kitą serverį." + "Pasirinktas serveris nepalaiko slaptažodžio ar OIDC prisijungimo. Susisiekite su serverio administracija arba pasirinkite kitą serverį." "Įveskite savo duomenis" "Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui." "Sveiki sugrįžę!" @@ -41,7 +41,6 @@ "Prisijungti prie %1$s" "Prisijungti su QR kodu" "Kurti paskyrą" - "Sveiki sugrįžę" "Sveiki atvykę į sparčiausią „%1$s“ kada nors. Pagerintas spartai ir paprastumui." "Sveiki atvykę į „%1$s“. Pagerintas spartai ir paprastumui." "Būkite savo stichijoje" 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 6645691b84..3233329f86 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 OAuth-pålogging. Ta kontakt med administratoren din eller velg en annen hjemmeserver." + "Den valgte hjemmeserveren støtter ikke passord eller OIDC-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!" @@ -42,7 +42,6 @@ "Logg inn på %1$s" "Logg inn med QR-kode" "Opprett konto" - "Velkommen tilbake" "Velkommen til den raskeste %1$s noensinne. Superladet for hastighet og enkelhet." "Velkommen til %1$s. Supercharged, for hastighet og enkelhet." "Vær i ditt rette element" 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 e5d90e2234..8a7f695156 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 OAuth aanmelding. Neem contact op met je beheerder of kies een andere homeserver." + "De geselecteerde homeserver ondersteunt geen wachtwoord of OIDC 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 ca0d035fe0..0c7810abe7 100644 --- a/features/login/impl/src/main/res/values-pl/translations.xml +++ b/features/login/impl/src/main/res/values-pl/translations.xml @@ -28,29 +28,20 @@ "Jaki jest adres Twojego serwera?" "Wybierz swój serwer" "Utwórz konto" - "To konto zostało usunięte." + "To konto zostało dezaktywowane." "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 OAuth. Skontaktuj się z jego administratorem lub wybierz inny serwer domowy." + "Wybrany serwer domowy nie obsługuje uwierzytelniania hasłem, ani OIDC. 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" @@ -69,8 +60,6 @@ "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. 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 ad2ad2b5fb..afca14d201 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 OAuth. Entre em contato com o administrador ou escolha outro servidor." + "O servidor selecionado não suporta a entrada por senha ou OIDC. 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 ff3cae843d..c2aa0d5ed6 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 eliminada." + "Esta conta foi desativada." "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 OAuth. 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 OIDC. 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 b44242abf8..6dddc4d0bf 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -28,29 +28,20 @@ "Care este adresa serverului dumneavoastră?" "Selectați serverul dumneavoastra" "Creați un cont" - "Acest cont a fost șters." + "Acest cont a fost dezactivat." "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 OAuth. Te rugăm să contactezi administratorul sau să alegi un alt homeserver." + "Homeserver-ul selectat nu acceptă autentificarea prin parola sau OIDC. 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" @@ -69,8 +60,6 @@ "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 564fc87cac..b967224f2e 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -28,29 +28,20 @@ "Какой адрес у вашего сервера?" "Выберите свой сервер" "Создать аккаунт" - "Эта учетная запись была удалена." + "Данная учётная запись была отключена." "Неверное имя пользователя и/или пароль" "Это некорректный идентификатор пользователя. Правильный формат: @user:homeserver.org" "Этот сервер настроен на использование токенов обновления. Они не поддерживаются при использовании входа на основе пароля." - "Выбранный сервер не поддерживает вход по паролю и OAuth. Пожалуйста, свяжитесь с администратором или выберите другой сервер." + "Выбранный сервер не поддерживает вход по паролю и OIDC. Пожалуйста, свяжитесь с администратором или выберите другой сервер." "Введите свои данные" "Matrix — это открытая сеть для безопасной децентрализованной связи." "Рады видеть вас снова!" "Войти в %1$s" - "Открыть Element Classic" - "Откройте Element Classic на своем устройстве." - "Перейдите в Настройки > Безопасность и конфиденциальность" - "В разделе «Управление криптографическими ключами» выбери «Восстановление зашифрованных сообщений»" - "Следуйте инструкциям, чтобы активировать хранилище ключей" - "Вернитесь к %1$s" - "Перед продолжением активируйте хранилище ключей %1$s" "Версия %1$s" - "Проверка аккаунта" "Войти" "Войти в %1$s" "Войти с QR-кодом" "Создать аккаунт" - "С возвращением" "Добро пожаловать в быстрый и простой %1$s." "Добро пожаловать в быстрый и простой %1$s." "Элементарно." 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 b2c3077de2..9bd494dd56 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 OAuth. Obráťte sa na správcu alebo vyberte iný domovský server." + "Vybraný domovský server nepodporuje prihlásenie pomocou hesla alebo OIDC. 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 396f2025b7..33fb76b5bd 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 OAuth-inloggning. Kontakta administratören eller välj en annan hemserver." + "Den valda hemservern stöder inte lösenord eller OIDC-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 05bb9bab15..1574fca3a8 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 OAuth 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 OIDC 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 17632cc4fc..b93a66fed2 100644 --- a/features/login/impl/src/main/res/values-uk/translations.xml +++ b/features/login/impl/src/main/res/values-uk/translations.xml @@ -28,29 +28,20 @@ "Яка адреса вашого сервера?" "Виберіть свій сервер" "Створити обліковий запис" - "Цей обліковий запис було видалено." + "Цей обліковий запис було деактивовано." "Неправильне ім\'я користувача та/або пароль" "Це недійсний ідентифікатор користувача. Очікуваний формат: \'@user:homeserver.org\'" "Цей сервер налаштований на використання оновлюваних токенів. Вони не підтримуються, якщо використовується вхід за допомогою основі пароля." - "Обраний домашній сервер не підтримує вхід за допомогою пароля або OAuth. Зверніться до адміністратора або виберіть інший домашній сервер." + "Обраний домашній сервер не підтримує вхід за допомогою пароля або OIDC. Зверніться до адміністратора або виберіть інший домашній сервер." "Введіть свої дані" "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 6762ab16af..334d5b6ea6 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 @@ "غلط صارف نام اور/یا لفظ عبور" "یہ صالح صارف شناسه نہیں ہے۔ متوقع شکل: @صارف:منزلی خادم" "یہ خادم تازگی کی رموزِ ممیز استعمال کرنے کے لئے تشکیل دیا گیا ہے۔ لفظ عبور پر مبنی دخول استعمال کرتے ہوئے ان کی حمایت نہیں کی جاتی۔" - "منتخب منزلی خادم کلمۂ عبوری یا OAuth دخول کا تعاون نہیں کرتا۔ برائے مہربانی اپنے منتظم سے رابطہ کریں یا کوئی اور منزلی خادم چنیں۔" + "منتخب منزلی خادم کلمۂ عبوری یا OIDC دخول کا تعاون نہیں کرتا۔ برائے مہربانی اپنے منتظم سے رابطہ کریں یا کوئی اور منزلی خادم چنیں۔" "اپنی تفصیلات درج کریں" "میٹرکس محفوظ، غیر مرکزی مواصلت کے لئے ایک کھلا شبکہ ہے۔" "واپس خوش آمدید!" 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 546397935b..54a1cf11cc 100644 --- a/features/login/impl/src/main/res/values-uz/translations.xml +++ b/features/login/impl/src/main/res/values-uz/translations.xml @@ -31,25 +31,16 @@ "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 OAuth loginni qo\'lab-quvvatlamaydi. Iltimos, administratoringizga murojaat qiling yoki boshqa uy serverini tanlang." + "Tanlangan uy serveri parol yoki OIDC 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!" "Kirish%1$s" - "Element Classic ilovasini ochish" - "Element Classic ilovasini qurilmada oching" - "Sozlamalar > Xavfsizlik va maxfiylik bo‘limiga kiring" - "Kriptografiya kalitlarini boshqarishda Shifrlangan xabarlarni tiklash bandini tanlang" - "Kalit xotirasini yoqish uchun ko‘rsatmalarga amal qiling" - "%1$sga qaytish" - "%1$s xizmatiga o‘tishdan oldin kalit xotirasini yoqing" "%1$s versiya" - "Joriy hisob" "Qo\'lda tizimga kiring" "Kirish%1$s" "QR kod bilan tizimga kiring" "Hisob yaratish" - "Xush kelibsiz." "Eng tezkor %1$sga xush kelibsiz. Tezlik va oddiylik uchun super zaryadlangan." "%1$sga Xush kelibsiz. Tezlik va oddiylik uchun o\'ta zaryadlangan." "Elementingizda bo\'ling" @@ -68,8 +59,6 @@ "Tizimga kirish soʻrovi bekor qilindi" "Boshqa qurilmadan hisobga kirish bekor qilindi." "Tizimga kirish rad etildi" - "Boshqa hech narsa qilishingiz shart emas." - "Boshqa qurilmangiz allaqachon tizimga kirgan" "Kirish muddati tugagan. Iltimos, qayta urinib koʻring." "Kirish oʻz vaqtida tugallanmagan" "Boshqa qurilmangiz %s hisobiga QR kod orqali kirishni qoʻllab-quvvatlamaydi. 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 66089a4708..b22ba5de7c 100644 --- a/features/login/impl/src/main/res/values-vi/translations.xml +++ b/features/login/impl/src/main/res/values-vi/translations.xml @@ -13,16 +13,9 @@ "Khác" "Sử dụng nhà cung cấp tài khoản khác, ví dụ như máy chủ riêng của bạn hoặc tài khoản công việc." "Thay đổi nhà cung cấp tài khoản" - "Google Play" - "Ứng dụng Element Pro là bắt buộc trên %1$s. Vui lòng tải xuống từ cửa hàng." - "Element Pro là bắt buộc" "Chúng tớ không thể kết nối với homeserver này. Vui lòng kiểm tra xem cậu đã nhập URL homeserver chính xác chưa. Nếu URL chính xác, hãy liên hệ với quản trị viên homeserver để được hỗ trợ thêm." "Máy chủ không khả dụng do sự cố trong tệp .well-known: %1$s" - "Nhà cung cấp tài khoản đã chọn không hỗ trợ đồng bộ sliding. Cần nâng cấp máy chủ để sử dụng %1$s ." - "%1$s không được phép kết nối với %2$s ." - "Ứng dụng này đã được cấu hình để cho phép: %1$s ." - "Không cho phép nhà cung cấp tài khoản %1$s." "URL homeserver" "Địa chỉ máy chủ của bạn là gì?" "Chọn máy chủ của bạn" @@ -31,7 +24,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 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." + "Homeserver đã chọn không hỗ trợ đăng nhập bằng mật khẩu hoặc OIDC. Vui lòng liên hệ với quản trị viên của cậu hoặc chọn một homeserver khác." "Nhập thông tin chi tiết của bạn." "Matrix là một mạng mở cho việc liên lạc an toàn và phi tập trung." "Chào mừng bạn quay trở lại!" 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 c4d16f5928..1b0b94d7a6 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,25 +32,16 @@ "不正確的使用者名稱或密碼" "此非有效的使用者識別字串。預期的格式:‘@user:homeserver.org’" "此伺服器已設定為使用重新整理權杖。使用以密碼為基礎的登入方式時,不支援這些功能。" - "選定的家伺服器不支援密碼或 OAuth 登入。請聯絡您的管理員或選擇其他家伺服器。" + "選定的家伺服器不支援密碼或 OIDC 登入。請聯絡您的管理員或選擇其他家伺服器。" "輸入您的詳細資料" "Matrix 是一個開放網路,為了安全且去中心化的通訊而生。" "歡迎回來!" "登入 %1$s" - "開啟 Element Classic" - "在您的裝置上開啟 Element Classic" - "前往「設定」→「安全性與隱私權」" - "在密碼學金鑰管理中,選取加密訊息還原" - "按照說明啟用您的金鑰儲存空間" - "回到 %1$s" - "請先啟用您的金鑰儲存空間,然後再繼續 %1$s" "版本 %1$s" - "檢查帳號" "手動登入" "登入 %1$s" "使用 QR code 登入" "建立帳號" - "歡迎回來" "歡迎使用有史以來最快的 %1$s。速度超快,操作簡便。" "歡迎使用 %1$s。速度超快且簡單。" "Be in your element" @@ -69,8 +60,6 @@ "已取消登入請求" "其他裝置拒絕登入。" "已拒絕登入" - "您不需要進行其他操作。" - "您的其他裝置已登入" "登入已過期。請再試一次。" "未及時完成登入" "您的其他裝置不支援使用 QR cpde 登入 %s。 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 16afe7d5bd..fd8105b71e 100644 --- a/features/login/impl/src/main/res/values-zh/translations.xml +++ b/features/login/impl/src/main/res/values-zh/translations.xml @@ -1,109 +1,100 @@ - "更改账户提供者" - "主服务器地址" + "更改账户提供方" + "服务器地址" "输入搜索词或域名地址。" "搜索公司、社区或私人服务器。" - "查找账户提供者" - "这是你的对话将存在的地方,就像你使用邮件提供者来存储电子邮件那样。" - "你即将登录到 %s" - "这是你的对话将存在的地方,就像你使用邮件提供者来存储电子邮件那样。" - "你即将在 %s 上创建账户" + "寻找账户提供方" + "这是您的对话将存在的地方,就像您使用电子邮件提供方来保存电子邮件一样。" + "您即将登录 %s" + "这是您的对话将存在的地方,就像您使用电子邮件提供方来保存电子邮件一样。" + "您即将在 %s 上创建一个帐户" "Matrix.org 由 Matrix.org 基金会运营,是用于安全、去中心化的通信的公共 Matrix 网络上的大型免费服务器。" - "其它" - "使用其它账户提供者,例如你自己的私有服务器或工作账户。" - "更改账户提供者" + "其他" + "使用其他账户提供商,例如您自己的私人服务器或工作账户。" + "更改账户提供方" "Google Play" - "%1$s 要求 Element Pro。请从应用商店下载。" - "需要 Element Pro" - "我们无法访问此服务器。请检查输入的服务器 URL 是否正确。如果 URL 正确,请联系服务器管理员寻求进一步帮助。" - "由于 .well-known 文件存在问题,服务器不可用: + "%1$s 需要 Element Pro 应用。请从应用商店下载。" + "需要 Element Pro 版" + "我们无法访问此服务器。请检查您输入的服务器网址是否正确。如果 URL 正确,请联系您的服务器管理员寻求进一步帮助。" + "由于 .well-known 文件中存在问题,服务器不可用: %1$s" - "所选账户提供者不支持滑动同步。需要升级服务器才能使用 %1$s。" - "%1$s 不允许连接到 %2$s。" - "此 app 已配置为允许访问:%1$s。" - "账户提供者 %1$s 不被允许。" - "主服务器 URL" + "所选账户提供商不支持跨屏同步。需要升级服务器才能使用%1$s。" + "%1$s不允许连接到%2$s。" + "本应用已配置为允许访问:%1$s 。" + "账户提供商%1$s 不被允许。" + "服务器网址" "输入域名地址。" - "你的服务器地址是什么?" + "您的服务器地址是什么?" "选择服务器" "创建账户" - "此账户已被删除。" - "用户名与(或)密码不正确" - "这不是合法的用户 ID。预期格式:“@user:homeserver.org”。" + "该账户已被停用。" + "错误的用户名和/或密码" + "这不是合法的用户 ID。期望格式:‘@user:homeserver.org’。" "此服务器使用刷新令牌。使用密码登录时不支持这些功能。" - "该服务器不支持密码登录与 OAuth 登录。请联系服务器管理员或选择另一服务器。" - "输入详细信息" + "该服务器不支持密码登录和 OIDC 第三方账户登录。请联系服务器管理员,或选择别的服务器。" + "输入您的详细信息" "Matrix 是一个用于安全、去中心化通信的开放网络。" "欢迎回来!" "登录到 %1$s" - "打开 Element Classic" - "在你的设备上打开 Element Classic" - "前往“设置” > “安全与隐私”" - "在加密密钥管理中选择“恢复加密消息”。" - "按指示启用密钥存储" - "返回到 %1$s" - "请先启用密钥存储再继续处理 %1$s" "版本%1$s" - "正在检查账户" "手动登录" "登录到 %1$s" "使用二维码登录" "创建账户" - "欢迎回来" - "欢迎使用迄今最快的 %1$s,速度与简洁的极致。" + "欢迎使用 %1$s,快而简约的消息应用。" "欢迎使用 %1$s,速度与简洁的极致。" - "融入 Element" + "融入您的 Element" "建立安全连接" - "无法与新设备建立安全连接。你的现有设备仍然安全,无需担心。" + "无法与新设备建立安全连接。您现有的设备仍然安全,无需担心。" "现在怎么办?" "如果这是网络问题,请尝试使用二维码再次登录" "如果遇到同样的问题,请尝试使用不同的 WiFi 网络或使用移动数据代替 WiFi" "如果不起作用,请手动登录" "连接不安全" - "你将被要求输入此设备上显示的两位数字。" - "在你的其它设备上输入以下数字" - "在其它设备登录后重试,或使用另一个已登录的设备。" - "尚未登录的其它设备" + "您会被要求输入此设备上显示的两位数。" + "在您的其他设备上输入下面的数字" + "在其他设备登录后重试,或使用另一个已登录的设备。" + "其他设备未登录" "登录被另一台设备取消" "登录请求已取消" - "另一设备上的登录请求已被拒绝。" + "其它设备未接受请求" "登录被拒绝" - "无需额外操作。" - "你已在另一设备上登录。" + "您无需额外操作。" + "您已在另一台设备登录。" "登录已过期. 请重试." "登录未及时完成" "另一个设备不支持使用二维码登录 %s. 尝试手动或使用另一个设备扫描二维码." - "二维码不受支持" - "账户提供者不支持 %1$s." + "不支持二维码" + "账户提供方不支持 %1$s." "不支持 %1$s." "准备进行扫描" "在桌面设备上打开 %1$s" "点击你的头像" "选择 %1$s" - "“关联新设备”" + "「连接新设备」" "使用此设备扫描二维码" - "仅在账户提供者支持时可用。" + "仅在您的账户提供方支持时才可用。" "在另一台设备上打开 %1$s 以获取二维码" - "使用其它设备上显示的二维码。" - "重试" + "使用其他设备上显示的二维码。" + "再试一次" "二维码错误" "转到摄像头设置" - "你需要授予 %1$s 使用设备摄像头的权限才能继续。" - "允许访问摄像头以扫描二维码" + "您需要授予 %1$s 使用设备摄像头的权限才能继续。" + "允许摄像头权限以扫描 QR 码" "扫描二维码" "重新开始" "发生了意外错误。请再试一次。" - "正在等待其它设备" - "你的账户提供者可能会要求你提供以下代码以验证登录。" - "你的验证码" - "更改账户提供者" + "等着您的其他设备" + "您的账户提供方可能会要求您提供以下代码来验证登录。" + "您的验证码" + "更改账户提供方" "专为 Element 员工提供的私人服务器。" "Matrix 是一个用于安全、去中心化通信的开放网络。" - "这是你的对话将存在的地方,就像你使用邮件提供者来存储电子邮件那样。" + "这是您的对话将存在的地方,就像您使用电子邮件提供方来保存电子邮件一样。" "即将登录 %1$s" - "选择账户提供者" + "选择账户提供商" "即将在 %1$s 上创建一个账户" diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 10fb6ef04a..b4dee32721 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -28,11 +28,11 @@ "What is the address of your server?" "Select your server" "Create account" - "This account has been deleted." + "This account has been deactivated." "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 OAuth login. Please contact your admin or choose another homeserver." + "The selected homeserver doesn\'t support password or OIDC login. Please contact your admin or choose another homeserver." "Enter your details" "Matrix is an open network for secure, decentralised communication." "Welcome back!" @@ -40,12 +40,11 @@ "Open Element Classic" "Open Element Classic on your device" "Go to Settings > Security & Privacy" - "In Cryptography keys management, select Encrypted messages recovery" + "In Cryptography keys management, select Encrypted message recovery" "Follow the instructions to enable your key storage" "Come back to %1$s" "Enable your key storage before proceeding to %1$s" "Version %1$s" - "Checking account" "Sign in manually" "Sign in to %1$s" "Sign in with QR code" 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 a05194d008..86a629270f 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt @@ -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.oauth.test.customtab.FakeOAuthActionFlow +import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode import kotlinx.coroutines.test.runTest @@ -39,7 +39,7 @@ class DefaultLoginEntryPointTest { buildContext = buildContext, plugins = plugins, accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), - oAuthActionFlow = FakeOAuthActionFlow(), + oidcActionFlow = FakeOidcActionFlow(), appCoroutineScope = backgroundScope, elementClassicConnection = FakeElementClassicConnection(), preferencesEntryPoint = FakePreferencesEntryPoint(), 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 274b58ee49..1fb5d37627 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(supportsOAuthLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOidcLogin = 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 8ea1b2e3d3..5da3c97f3c 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,6 +15,9 @@ 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 @@ -109,6 +112,21 @@ 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( @@ -496,10 +514,17 @@ 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 112d8d7108..9d2628005c 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 @@ -79,7 +79,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.OAuthMetadataInvalid) + qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.OidcMetadataInvalid) 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 61ec7cc698..f7ff5d384d 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,20 +6,17 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -28,31 +25,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 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 ChooseAccountProviderViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes the expected callback`() { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { - setChooseAccountProviderView( + rule.setChooseAccountProviderView( state = aChooseAccountProviderState( eventSink = eventSink, ), onBackClick = it, ) - pressBack() + rule.pressBack() } } @Config(qualifiers = "h1024dp") @Test - fun `selecting an account provider emits the the expected event`() = runAndroidComposeUiTest { + fun `selecting an account provider emits the the expected event`() { val eventSink = EventsRecorder() - setChooseAccountProviderView( + rule.setChooseAccountProviderView( state = aChooseAccountProviderState( accountProviders = listOf( ChooseAccountProviderPresenterTest.accountProvider1, @@ -62,27 +64,27 @@ class ChooseAccountProviderViewTest { eventSink = eventSink, ), ) - onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick() + rule.onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick() eventSink.assertSingle(ChooseAccountProviderEvents.SelectAccountProvider(ChooseAccountProviderPresenterTest.accountProvider1)) } @Test - fun `when error is displayed - closing the dialog emits the expected event`() = runAndroidComposeUiTest { + fun `when error is displayed - closing the dialog emits the expected event`() { val eventSink = EventsRecorder() - setChooseAccountProviderView( + rule.setChooseAccountProviderView( state = aChooseAccountProviderState( loginMode = AsyncData.Failure(AN_EXCEPTION), eventSink = eventSink, ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventSink.assertSingle(ChooseAccountProviderEvents.ClearError) } - private fun AndroidComposeUiTest.setChooseAccountProviderView( + private fun AndroidComposeTestRule.setChooseAccountProviderView( state: ChooseAccountProviderState, onBackClick: () -> Unit = EnsureNeverCalled(), - onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(), + onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(), onNeedLoginPassword: () -> Unit = EnsureNeverCalled(), onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(), @@ -91,7 +93,7 @@ class ChooseAccountProviderViewTest { ChooseAccountProviderView( state = state, onBackClick = onBackClick, - onOAuthDetails = onOAuthDetails, + onOidcDetails = onOidcDetails, onNeedLoginPassword = onNeedLoginPassword, onLearnMoreClick = onLearnMoreClick, onCreateAccountContinue = onCreateAccountContinue, 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 a9045ab152..6372841250 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.oauth.api.OAuthAction -import io.element.android.libraries.oauth.api.OAuthActionFlow -import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow +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.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(supportsOAuthLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOidcLogin = 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.OAuth::class.java) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) } } @Test - fun `present - OAuth - cancel with failure`() = runTest { + fun `present - oidc - cancel with failure`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) }, ) - val defaultOAuthActionFlow = FakeOAuthActionFlow() + val defaultOidcActionFlow = FakeOidcActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, - defaultOAuthActionFlow = defaultOAuthActionFlow, + defaultOidcActionFlow = defaultOidcActionFlow, ) 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.OAuth::class.java) - authenticationService.givenOAuthCancelError(AN_EXCEPTION) - defaultOAuthActionFlow.post(OAuthAction.GoBack()) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) + authenticationService.givenOidcCancelError(AN_EXCEPTION) + defaultOidcActionFlow.post(OidcAction.GoBack()) val cancelFailureState = awaitItem() assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @Test - fun `present - OAuth - cancel with success`() = runTest { + fun `present - oidc - cancel with success`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) }, ) - val defaultOAuthActionFlow = FakeOAuthActionFlow() + val defaultOidcActionFlow = FakeOidcActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, - defaultOAuthActionFlow = defaultOAuthActionFlow, + defaultOidcActionFlow = defaultOidcActionFlow, ) 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.OAuth::class.java) - defaultOAuthActionFlow.post(OAuthAction.GoBack()) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) + defaultOidcActionFlow.post(OidcAction.GoBack()) val cancelFinalState = awaitItem() assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @Test - fun `present - OAuth - cancel to unblock`() = runTest { + fun `present - oidc - cancel to unblock`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) }, ) - val defaultOAuthActionFlow = FakeOAuthActionFlow() + val defaultOidcActionFlow = FakeOidcActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, - defaultOAuthActionFlow = defaultOAuthActionFlow, + defaultOidcActionFlow = defaultOidcActionFlow, ) 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) - defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true)) + defaultOidcActionFlow.post(OidcAction.GoBack(toUnblock = true)) val cancelFinalState = awaitItem() assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @Test - fun `present - OAuth - success with failure`() = runTest { + fun `present - oidc - success with failure`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) }, ) - val defaultOAuthActionFlow = FakeOAuthActionFlow() + val defaultOidcActionFlow = FakeOidcActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, - defaultOAuthActionFlow = defaultOAuthActionFlow, + defaultOidcActionFlow = defaultOidcActionFlow, ) 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.OAuth::class.java) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) authenticationService.givenLoginError(AN_EXCEPTION) - defaultOAuthActionFlow.post(OAuthAction.Success("aUrl")) + defaultOidcActionFlow.post(OidcAction.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 - OAuth - success with success`() = runTest { + fun `present - oidc - success with success`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) }, ) - val defaultOidcActionFlow = FakeOAuthActionFlow() + val defaultOidcActionFlow = FakeOidcActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, - defaultOAuthActionFlow = defaultOidcActionFlow, + defaultOidcActionFlow = 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.OAuth::class.java) - defaultOidcActionFlow.post(OAuthAction.Success("aUrl")) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) + defaultOidcActionFlow.post(OidcAction.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 OAuth is successful`() = runTest { + fun `present - confirm account creation with oidc is successful`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOidcLogin = 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.OAuth::class.java) + assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) } } @Test - fun `present - confirm account creation with OAuth and url continues with OAuth`() = runTest { + fun `present - confirm account creation with oidc and url continues with oidc`() = runTest { val aUrl = "aUrl" val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOidcLogin = 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.OAuth::class.java) + assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) } } @Test - fun `present - confirm account creation without OAuth and with url continuing with url`() = runTest { + fun `present - confirm account creation without oidc 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(), - defaultOAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), + defaultOidcActionFlow: OidcActionFlow = FakeOidcActionFlow(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), ) = ConfirmAccountProviderPresenter( params = params, accountProviderDataSource = accountProviderDataSource, loginHelper = createLoginHelper( authenticationService = matrixAuthenticationService, - oAuthActionFlow = defaultOAuthActionFlow, + oidcActionFlow = defaultOidcActionFlow, 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 c0e7e5c378..26da50da63 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,23 +6,20 @@ * 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_USER_NAME @@ -33,154 +30,158 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class LoginPasswordViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { + fun `clicking on back invoke back callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setLoginPasswordView( + rule.setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder ), onBackClick = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `changing login invokes the expected event`() = runAndroidComposeUiTest { + fun `changing login invokes the expected event`() { val eventsRecorder = EventsRecorder() - setLoginPasswordView( + rule.setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, ), ) - val userNameHint = activity!!.getString(CommonStrings.common_username) - onNodeWithText(userNameHint).performTextInput(A_USER_NAME) + val userNameHint = rule.activity.getString(CommonStrings.common_username) + rule.onNodeWithText(userNameHint).performTextInput(A_USER_NAME) eventsRecorder.assertSingle( LoginPasswordEvents.SetLogin(A_USER_NAME) ) } @Test - fun `changing login removes new lines the expected event`() = runAndroidComposeUiTest { + fun `changing login removes new lines the expected event`() { val eventsRecorder = EventsRecorder() - setLoginPasswordView( + rule.setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, ), ) - val userNameHint = activity!!.getString(CommonStrings.common_username) - onNodeWithText(userNameHint).performTextInput("a\nb") + val userNameHint = rule.activity.getString(CommonStrings.common_username) + rule.onNodeWithText(userNameHint).performTextInput("a\nb") eventsRecorder.assertSingle( LoginPasswordEvents.SetLogin("ab") ) } @Test - fun `clearing login invokes the expected event`() = runAndroidComposeUiTest { + fun `clearing login invokes the expected event`() { val eventsRecorder = EventsRecorder() - setLoginPasswordView( + rule.setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(A_USER_NAME), eventSink = eventsRecorder, ), ) - val a11yClear = activity!!.getString(CommonStrings.action_clear) - onNodeWithContentDescription(a11yClear).performClick() + val a11yClear = rule.activity.getString(CommonStrings.action_clear) + rule.onNodeWithContentDescription(a11yClear).performClick() eventsRecorder.assertSingle( LoginPasswordEvents.SetLogin("") ) } @Test - fun `changing password invokes the expected event`() = runAndroidComposeUiTest { + fun `changing password invokes the expected event`() { val eventsRecorder = EventsRecorder() - setLoginPasswordView( + rule.setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, ), ) - val userNameHint = activity!!.getString(CommonStrings.common_password) - onNodeWithText(userNameHint).performTextInput(A_PASSWORD) + val userNameHint = rule.activity.getString(CommonStrings.common_password) + rule.onNodeWithText(userNameHint).performTextInput(A_PASSWORD) eventsRecorder.assertSingle( LoginPasswordEvents.SetPassword(A_PASSWORD) ) } @Test - fun `reveal password makes the password visible`() = runAndroidComposeUiTest { + fun `reveal password makes the password visible`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setLoginPasswordView( + rule.setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(password = A_PASSWORD), eventSink = eventsRecorder, ), ) - onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) - val resources = activity!!.resources + rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) // Show password - val a11yShowPassword = resources.getString(CommonStrings.a11y_show_password) - onNodeWithContentDescription(a11yShowPassword).performClick() - onNodeWithTag(TestTags.loginPassword.value).assert(hasText(A_PASSWORD)) + val a11yShowPassword = rule.activity.getString(CommonStrings.a11y_show_password) + rule.onNodeWithContentDescription(a11yShowPassword).performClick() + rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText(A_PASSWORD)) // Hide password - val a11yHidePassword = resources.getString(CommonStrings.a11y_hide_password) - onNodeWithContentDescription(a11yHidePassword).performClick() - onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) + val a11yHidePassword = rule.activity.getString(CommonStrings.a11y_hide_password) + rule.onNodeWithContentDescription(a11yHidePassword).performClick() + rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) } @Test - fun `when login is empty, continue button is not enabled`() = runAndroidComposeUiTest { + fun `when login is empty, continue button is not enabled`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setLoginPasswordView( + rule.setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(password = A_PASSWORD), eventSink = eventsRecorder, ), ) - val continueStr = activity!!.getString(CommonStrings.action_continue) - onNodeWithText(continueStr).assertIsNotEnabled() + val continueStr = rule.activity.getString(CommonStrings.action_continue) + rule.onNodeWithText(continueStr).assertIsNotEnabled() } @Test - fun `when password is empty, continue button is not enabled`() = runAndroidComposeUiTest { + fun `when password is empty, continue button is not enabled`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setLoginPasswordView( + rule.setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(login = A_USER_NAME), eventSink = eventsRecorder, ), ) - val continueStr = activity!!.getString(CommonStrings.action_continue) - onNodeWithText(continueStr).assertIsNotEnabled() + val continueStr = rule.activity.getString(CommonStrings.action_continue) + rule.onNodeWithText(continueStr).assertIsNotEnabled() } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Continue sends expected event`() = runAndroidComposeUiTest { + fun `clicking on Continue sends expected event`() { val eventsRecorder = EventsRecorder() - setLoginPasswordView( + rule.setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(login = A_USER_NAME, password = A_PASSWORD), eventSink = eventsRecorder, ), ) - val continueStr = activity!!.getString(CommonStrings.action_continue) - onNodeWithText(continueStr).assertIsEnabled() - clickOn(CommonStrings.action_continue) + val continueStr = rule.activity.getString(CommonStrings.action_continue) + rule.onNodeWithText(continueStr).assertIsEnabled() + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle( LoginPasswordEvents.Submit ) } } -private fun AndroidComposeUiTest.setLoginPasswordView( +private fun AndroidComposeTestRule.setLoginPasswordView( state: LoginPasswordState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { 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 8249694278..1fdfb7e070 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -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.oauth.api.OAuthActionFlow -import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow +import io.element.android.libraries.oidc.api.OidcActionFlow +import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow 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( - oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), + oidcActionFlow: OidcActionFlow = FakeOidcActionFlow(), authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), ): LoginHelper = LoginHelper( - oAuthActionFlow = oAuthActionFlow, + oidcActionFlow = oidcActionFlow, 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 bcb62ea707..ad09445075 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -6,23 +6,20 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.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.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -32,17 +29,22 @@ 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 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() + @Test - fun `when can create account - clicking on create account calls the expected callback`() = runAndroidComposeUiTest { + fun `when can create account - clicking on create account calls the expected callback`() { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( canCreateAccount = true, showDeveloperSettings = false, @@ -50,40 +52,40 @@ class OnboardingViewTest { ), onCreateAccount = callback, ) - clickOn(R.string.screen_onboarding_sign_up) + rule.clickOn(R.string.screen_onboarding_sign_up) // Developer settings should not be shown - val developerSettingsText = activity!!.getString(CommonStrings.common_developer_options) - onNodeWithContentDescription(developerSettingsText).assertDoesNotExist() + val developerSettingsText = rule.activity.getString(CommonStrings.common_developer_options) + rule.onNodeWithContentDescription(developerSettingsText).assertDoesNotExist() } } @Test - fun `when can go back - clicking on back calls the expected callback`() = runAndroidComposeUiTest { + fun `when can go back - clicking on back calls the expected callback`() { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( isAddingAccount = true, eventSink = eventSink, ), onBackClick = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() = runAndroidComposeUiTest { + fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( canLoginWithQrCode = true, eventSink = eventSink, ), onSignInWithQrCode = callback, ) - clickOn(R.string.screen_onboarding_sign_in_with_qr_code) + rule.clickOn(R.string.screen_onboarding_sign_in_with_qr_code) } } @@ -93,10 +95,10 @@ class OnboardingViewTest { "can search account provider" to false, "cannot search account provider" to true, ) - ) = runAndroidComposeUiTest { + ) { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( canLoginWithQrCode = true, mustChooseAccountProvider = mustChooseAccountProvider, @@ -104,7 +106,7 @@ class OnboardingViewTest { ), onSignIn = callback, ) - clickOn(R.string.screen_onboarding_sign_in_manually) + rule.clickOn(R.string.screen_onboarding_sign_in_manually) } } @@ -114,10 +116,10 @@ class OnboardingViewTest { "can search account provider" to false, "cannot search account provider" to true, ) - ) = runAndroidComposeUiTest { + ) { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( canLoginWithQrCode = false, canCreateAccount = false, @@ -126,89 +128,89 @@ class OnboardingViewTest { ), onSignIn = callback, ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) } } @Test - fun `when sign in to pre defined account provider - clicking on button emits the expected event`() = runAndroidComposeUiTest { + fun `when sign in to pre defined account provider - clicking on button emits the expected event`() { val eventSink = EventsRecorder() - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", eventSink = eventSink, ), ) - val buttonText = activity!!.getString(R.string.screen_onboarding_sign_in_to, "element.io") - onNodeWithText(buttonText).performClick() + val buttonText = rule.activity.getString(R.string.screen_onboarding_sign_in_to, "element.io") + rule.onNodeWithText(buttonText).performClick() eventSink.assertSingle(OnBoardingEvents.OnSignIn("element.io")) } @Test - fun `when error is displayed - closing the dialog emits the expected event`() = runAndroidComposeUiTest { + fun `when error is displayed - closing the dialog emits the expected event`() { val eventSink = EventsRecorder() - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", loginMode = AsyncData.Failure(AN_EXCEPTION), eventSink = eventSink, ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventSink.assertSingle(OnBoardingEvents.ClearError) } @Test - fun `clicking on report a problem calls the sign in callback`() = runAndroidComposeUiTest { + fun `clicking on report a problem calls the sign in callback`() { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( canReportBug = true, eventSink = eventSink, ), onReportProblem = callback, ) - val text = activity!!.getString(CommonStrings.common_report_a_problem) - onNodeWithText(text).assertExists() - clickOn(CommonStrings.common_report_a_problem) + val text = rule.activity.getString(CommonStrings.common_report_a_problem) + rule.onNodeWithText(text).assertExists() + rule.clickOn(CommonStrings.common_report_a_problem) } } @Test - fun `clicking on settings calls the developer settings callback`() = runAndroidComposeUiTest { + fun `clicking on settings calls the developer settings callback`() { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( showDeveloperSettings = true, eventSink = eventSink, ), onDeveloperSettingsClick = callback, ) - val text = activity!!.getString(CommonStrings.common_developer_options) - onNodeWithContentDescription(text).performClick() + val text = rule.activity.getString(CommonStrings.common_developer_options) + rule.onNodeWithContentDescription(text).performClick() } } @Test - fun `cannot report a problem when the feature is disabled`() = runAndroidComposeUiTest { + fun `cannot report a problem when the feature is disabled`() { val eventSink = EventsRecorder(expectEvents = false) - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( canReportBug = false, eventSink = eventSink, ), ) - val text = activity!!.getString(CommonStrings.common_report_a_problem) - onNodeWithText(text).assertDoesNotExist() + val text = rule.activity.getString(CommonStrings.common_report_a_problem) + rule.onNodeWithText(text).assertDoesNotExist() } @Test - fun `when success PasswordLogin - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { + fun `when success PasswordLogin - the expected callback is invoked and the event is received`() { val eventSink = EventsRecorder() ensureCalledOnce { callback -> - setOnboardingView( + rule.setOnboardingView( state = anOnBoardingState( loginMode = AsyncData.Success(LoginMode.PasswordLogin), eventSink = eventSink, @@ -220,27 +222,27 @@ class OnboardingViewTest { } @Test - fun `when success Oidc - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { + fun `when success Oidc - the expected callback is invoked and the event is received`() { val eventSink = EventsRecorder() - val oAuthDetails = OAuthDetails("aUrl") - ensureCalledOnceWithParam(oAuthDetails) { callback -> - setOnboardingView( + val oidcDetails = OidcDetails("aUrl") + ensureCalledOnceWithParam(oidcDetails) { callback -> + rule.setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)), + loginMode = AsyncData.Success(LoginMode.Oidc(oidcDetails)), eventSink = eventSink, ), - onOAuthDetails = callback, + onOidcDetails = callback, ) } eventSink.assertSingle(OnBoardingEvents.ClearError) } @Test - fun `when success AccountCreation - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { + fun `when success AccountCreation - the expected callback is invoked and the event is received`() { val eventSink = EventsRecorder() - val oAuthDetails = OAuthDetails("aUrl") - ensureCalledOnceWithParam(oAuthDetails.url) { callback -> - setOnboardingView( + val oidcDetails = OidcDetails("aUrl") + ensureCalledOnceWithParam(oidcDetails.url) { callback -> + rule.setOnboardingView( state = anOnBoardingState( loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")), eventSink = eventSink, @@ -251,7 +253,7 @@ class OnboardingViewTest { eventSink.assertSingle(OnBoardingEvents.ClearError) } - private fun AndroidComposeUiTest.setOnboardingView( + private fun AndroidComposeTestRule.setOnboardingView( state: OnBoardingState, onBackClick: () -> Unit = EnsureNeverCalled(), onDeveloperSettingsClick: () -> Unit = EnsureNeverCalled(), @@ -259,7 +261,7 @@ class OnboardingViewTest { onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(), onCreateAccount: () -> Unit = EnsureNeverCalled(), onReportProblem: () -> Unit = EnsureNeverCalled(), - onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(), + onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(), onNeedLoginPassword: () -> Unit = EnsureNeverCalled(), onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(), @@ -273,7 +275,7 @@ class OnboardingViewTest { onSignIn = onSignIn, onCreateAccount = onCreateAccount, onReportProblem = onReportProblem, - onOAuthDetails = onOAuthDetails, + onOidcDetails = onOidcDetails, 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 79566625c5..a0469a684e 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,47 +6,49 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QrCodeConfirmationViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { + fun `on back pressed - calls the expected callback`() { ensureCalledOnce { callback -> - setQrCodeConfirmationView( + rule.setQrCodeConfirmationView( step = QrCodeConfirmationStep.DisplayCheckCode("12"), onCancel = callback ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `on Cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest { + fun `on Cancel button clicked - calls the expected callback`() { ensureCalledOnce { callback -> - setQrCodeConfirmationView( + rule.setQrCodeConfirmationView( step = QrCodeConfirmationStep.DisplayVerificationCode("123456"), onCancel = callback ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) } } - private fun AndroidComposeUiTest.setQrCodeConfirmationView( + private fun AndroidComposeTestRule.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 2ae68c3485..de0f689220 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType import io.element.android.libraries.ui.strings.CommonStrings @@ -21,42 +18,47 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QrCodeErrorViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `on back pressed - calls the onCancel callback`() = runAndroidComposeUiTest { + fun `on back pressed - calls the onCancel callback`() { ensureCalledOnce { callback -> - setQrCodeErrorView( + rule.setQrCodeErrorView( onCancel = callback, ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `on try again button clicked - calls the expected callback`() = runAndroidComposeUiTest { + fun `on try again button clicked - calls the expected callback`() { ensureCalledOnce { callback -> - setQrCodeErrorView( + rule.setQrCodeErrorView( onRetry = callback, ) - clickOn(CommonStrings.action_try_again) + rule.clickOn(CommonStrings.action_try_again) } } @Test - fun `on cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest { + fun `on cancel button clicked - calls the expected callback`() { ensureCalledOnce { callback -> - setQrCodeErrorView( + rule.setQrCodeErrorView( onCancel = callback, ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) } } - private fun AndroidComposeUiTest.setQrCodeErrorView( + private fun AndroidComposeTestRule.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 c6812d3759..cec67e5011 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.login.impl.R import io.element.android.tests.testutils.EnsureNeverCalled @@ -22,37 +19,42 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QrCodeIntroViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { + fun `on back pressed - calls the expected callback`() { ensureCalledOnce { callback -> - setQrCodeIntroView( + rule.setQrCodeIntroView( state = aQrCodeIntroState(), onBackClicked = callback ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest { + fun `on back button clicked - calls the expected callback`() { ensureCalledOnce { callback -> - setQrCodeIntroView( + rule.setQrCodeIntroView( state = aQrCodeIntroState(), onBackClicked = callback ) - pressBack() + rule.pressBack() } } @Test - fun `when can continue - calls the expected callback`() = runAndroidComposeUiTest { + fun `when can continue - calls the expected callback`() { ensureCalledOnce { callback -> - setQrCodeIntroView( + rule.setQrCodeIntroView( state = aQrCodeIntroState(canContinue = true), onContinue = callback ) @@ -60,16 +62,16 @@ class QrCodeIntroViewTest { } @Test - fun `on submit button clicked - emits the Continue event`() = runAndroidComposeUiTest { + fun `on submit button clicked - emits the Continue event`() { val eventRecorder = EventsRecorder() - setQrCodeIntroView( + rule.setQrCodeIntroView( state = aQrCodeIntroState(eventSink = eventRecorder), ) - clickOn(R.string.screen_qr_code_login_initial_state_button_title) + rule.clickOn(R.string.screen_qr_code_login_initial_state_button_title) eventRecorder.assertSingle(QrCodeIntroEvents.Continue) } - private fun AndroidComposeUiTest.setQrCodeIntroView( + private fun AndroidComposeTestRule.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 bde960ef1a..b8becd545f 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,15 +6,12 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import io.element.android.libraries.architecture.AsyncAction @@ -27,11 +24,16 @@ import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBackKey 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() + private var provider: ProcessCameraProvider? = null @Before @@ -46,28 +48,28 @@ class QrCodeScanViewTest { } @Test - fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { + fun `on back pressed - calls the expected callback`() { ensureCalledOnce { callback -> - setQrCodeScanView( + rule.setQrCodeScanView( state = aQrCodeScanState(), onBackClick = callback ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `on QR code data ready - calls the expected callback`() = runAndroidComposeUiTest { + fun `on QR code data ready - calls the expected callback`() { val data = FakeMatrixQrCodeLoginData() ensureCalledOnceWithParam(data) { callback -> - setQrCodeScanView( + rule.setQrCodeScanView( state = aQrCodeScanState(authenticationAction = AsyncAction.Success(data)), onQrCodeDataReady = callback ) } } - private fun AndroidComposeUiTest.setQrCodeScanView( + private fun AndroidComposeTestRule.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 d5356ced63..8de7718980 100644 --- a/features/logout/impl/build.gradle.kts +++ b/features/logout/impl/build.gradle.kts @@ -35,7 +35,6 @@ 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-ca/translations.xml b/features/logout/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index eb0c82bb15..0000000000 --- a/features/logout/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - "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 0218642abc..8ebea45c0e 100644 --- a/features/logout/impl/src/main/res/values-de/translations.xml +++ b/features/logout/impl/src/main/res/values-de/translations.xml @@ -1,18 +1,18 @@ - "Bist du sicher, dass du dieses Gerät entfernen möchtest?" - "Dieses Gerät entfernen" - "Dieses Gerät entfernen" - "Gerät wird entfernt…" - "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." + "Möchtest du dich wirklich abmelden?" + "Abmelden" + "Abmelden" + "Abmelden…" + "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." "Deine Schlüssel werden noch gesichert" - "Bitte warte, bis der Vorgang abgeschlossen ist, bevor du dieses Gerät entfernst." + "Bitte warte, bis dieser Vorgang abgeschlossen ist, bevor du dich abmeldest." "Deine Schlüssel werden noch gesichert" - "Dieses Gerät entfernen" - "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" - "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" + "Abmelden" + "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten." + "Wiederherstellung nicht eingerichtet" + "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." + "Hast du deinen Wiederherstellungsschlüssel gespeichert?" 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 cd29e13354..4bdf169576 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 selle seadme eemaldada?" - "Eemalda see seade" - "Eemalda see seade" - "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." + "Kas sa oled kindel, et soovid välja logida?" + "Logi välja" + "Logi välja" + "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." "Sinu krüptovõtmed on veel varundamisel" - "Enne selle seadme eemaldamist palun oota, et pooleliolev toiming lõppeb." + "Enne väljalogimist palun oota, et pooleliolev toiming lõppeb." "Sinu krüptovõtmed on veel varundamisel" - "Eemalda see seade" - "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." + "Logi välja" + "Sa oled logimas välja oma viimasest sessioonist. Kui teed seda nüüd, siis kaotad ligipääsu oma krüptitud sõnumitele." "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." - "Enne selle seadme eemaldamist veendu, et sul on juurdepääs taastevõtmele" + "Kas sa oled oma taastevõtme salvestanud?" 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 a540b9be37..8dfaad8580 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-hr/translations.xml b/features/logout/impl/src/main/res/values-hr/translations.xml index ec8116a8c5..0a5d583a3c 100644 --- a/features/logout/impl/src/main/res/values-hr/translations.xml +++ b/features/logout/impl/src/main/res/values-hr/translations.xml @@ -1,18 +1,17 @@ - "Jeste li sigurni da želite ukloniti ovaj uređaj?" - "Ukloni ovaj uređaj" - "Ukloni ovaj uređaj" - "Uklanjanje uređaja…" - "Ovo je vaš jedini uređaj. Ako ga uklonite, trebat će vam ključ za oporavak kako biste potvrdili svoj digitalni identitet i vratili šifrirane razgovore sljedeći put kada se prijavite." - "Izgubiti ćete pristup svojim šifriranim chatovima" + "Jeste li sigurni da se želite odjaviti?" + "Odjava" + "Odjava" + "Odjavljivanje…" + "Odjavit ćete se iz svoje posljednje sesije. Ako se sada odjavite, nećete moći pristupiti svojim šifriranim porukama." + "Isključili ste sigurnosno kopiranje" "Vaši su se ključevi još uvijek sigurnosno kopirali kada ste se isključili iz mreže. Ponovno se povežite kako bi se vaši ključevi mogli sigurnosno kopirati prije nego što se odjavite." "Vaši se ključevi još uvijek sigurnosno kopiraju" - "Pričekajte da se ovo završi prije uklanjanja ovog uređaja." + "Pričekajte da se to dovrši prije nego što se odjavite." "Vaši se ključevi još uvijek sigurnosno kopiraju" - "Ukloni ovaj uređaj" - "Ovo je vaš jedini uređaj. Ako ga uklonite, trebat će vam ključ za oporavak kako biste potvrdili svoj digitalni identitet i vratili šifrirane razgovore sljedeći put kada se prijavite." - "Izgubit ćete pristup svojim šifriranim chatovima" - "Ovo je vaš jedini uređaj. Ako ga uklonite, trebat će vam ključ za oporavak kako biste potvrdili svoj digitalni identitet i vratili šifrirane razgovore sljedeći put kada se prijavite." - "Prije uklanjanja ovog uređaja provjerite imate li pristup ključu za oporavak" + "Odjava" + "Odjavit ćete se iz svoje posljednje sesije. Ako se sada odjavite, nećete moći pristupiti svojim šifriranim porukama." + "Oporavak nije postavljen" + "Odjavit ćete se iz svoje posljednje sesije. Ako se sada odjavite, možda nećete moći pristupiti svojim šifriranim porukama." 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 9d63573973..dabf83545e 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 non aktifkan device dari akun?" - "Hapus device dari akun" - "Hapus device dari akun" - "Mengeluarkan device dari akun…" + "Apakah Anda yakin ingin keluar dari akun?" + "Keluar dari akun" + "Keluar dari akun" + "Mengeluarkan 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" - "Hapus device dari akun" + "Keluar 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 691255f434..46a5c2d6bd 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 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." + "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." "Twoje klucze są nadal archiwizowane" - "Poczekaj na zakończenie procesu, zanim usuniesz to urządzenie." + "Zanim się wylogujesz, poczekaj na zakończenie operacji." "Twoje klucze są nadal archiwizowane" - "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" + "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?" 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 76e4f09d42..b8a7161c21 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 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." + "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." "As tuas chaves ainda estão a ser guardadas" - "Por favor, aguarda a conclusão desta operação antes de removeres o dispositivo." + "Por favor, aguarda a conclusão desta operação antes de terminares a sessão." "As tuas chaves ainda estão a ser guardadas" - "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" + "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?" diff --git a/features/logout/impl/src/main/res/values-ro/translations.xml b/features/logout/impl/src/main/res/values-ro/translations.xml index 1f1ff9e07a..7124188269 100644 --- a/features/logout/impl/src/main/res/values-ro/translations.xml +++ b/features/logout/impl/src/main/res/values-ro/translations.xml @@ -4,15 +4,15 @@ "Deconectați-vă" "Deconectați-vă" "Deconectare în curs…" - "Acesta este singurul dumneavoastră dispozitiv. Dacă îl eliminați, veți avea nevoie de o cheie de recuperare pentru a vă confirma identitatea digitală și a restaura mesajele criptate data viitoare când vă conectați." - "Sunteți pe cale să vă pierdeți accesul la mesajele dumneavoastră criptate." - "Cheile dumneavoastră erau încă în curs de backup atunci când ați fost deconectat. Reconectați-vă pentru ca cheile dumneavoastră să poată fi salvate înainte de a elimina acest dispozitiv." + "Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, veți pierde accesul la mesajele criptate." + "Ați dezactivat backup-ul" + "Cheile dumneavoastră erau încă în curs de backup atunci când ați fost deconectat. Reconectați-vă pentru ca cheile dumneavoastră să poată fi salvate înainte de a vă deconecta." "Cheile dumneavoastră sunt încă în curs de backup" "Vă rugăm să așteptați până la finalizarea acestui proces înainte de a vă deconecta." "Cheile dumneavoastră sunt încă în curs de backup" "Deconectați-vă" - "Acesta este singurul dumneavoastră dispozitiv. Dacă îl eliminați, veți avea nevoie de o cheie de recuperare pentru a vă confirma identitatea digitală și a restaura chat-urile criptate data viitoare când vă conectați." - "Sunteți pe cale să pierdeți accesul la mesajele dumneavoastră criptate" - "Acesta este singurul dumneavoastră dispozitiv. Dacă îl eliminați, veți avea nevoie de o cheie de recuperare pentru a vă confirma identitatea digitală și a restaura mesajele criptate data viitoare când vă conectați." - "Asigurați-vă că aveți acces la cheia de recuperare înainte de a elimina acest dispozitiv." + "Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, veți pierde accesul la mesajele criptate." + "Recuperarea nu este configurată" + "Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, este posibil să pierdeți accesul la mesajele criptate." + "Ați salvat cheia de recuperare?" 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 4fe07b2fa7..39301437fb 100644 --- a/features/logout/impl/src/main/res/values-sk/translations.xml +++ b/features/logout/impl/src/main/res/values-sk/translations.xml @@ -1,16 +1,16 @@ - "Naozaj chcete odstrániť toto zariadenie?" - "Odstrániť toto zariadenie" - "Odstrániť toto zariadenie" - "Odoberanie zariadenia…" + "Ste si istí, že sa chcete odhlásiť?" + "Odhlásiť sa" + "Odhlásiť sa" + "Prebieha odhlasovanie…" "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ú" - "Odstrániť toto zariadenie" + "Odhlásiť sa" "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é" "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." 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 f012603533..7e23189dc6 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-uz/translations.xml b/features/logout/impl/src/main/res/values-uz/translations.xml index a6b46bd5b5..4d03d9cfa3 100644 --- a/features/logout/impl/src/main/res/values-uz/translations.xml +++ b/features/logout/impl/src/main/res/values-uz/translations.xml @@ -1,18 +1,17 @@ "Haqiqatan ham tizimdan chiqmoqchimisiz?" - "Bu qurilmani olib tashlash" - "Bu qurilmani olib tashlash" + "Tizimdan chiqish" + "Tizimdan chiqish" "Chiqish…" - "Bu sizning yagona qurilmangiz. Agar uni olib tashlasangiz, keyingi safar hisobingizga kirganingizda raqamli shaxsingizni tasdiqlash va shifrlangan chatlaringizni tiklash uchun zaxira kaliti kerak bo‘ladi." - "Shifrlangan chatlarga ruxsat yopiladi" - "Oflaynga chiqqaningizda kalitlaringiz hali ham zaxiralanayotgan edi. Bu qurilmani olib tashlashdan oldin kalitlaringiz zaxiralanishi uchun qayta ulaning." + "Siz oxirgi sessiyangizdan chiqmoqdasiz. Agar hozir chiqib ketsangiz, shifrlangan xabarlaringizga kira olmaysiz." + "Siz zaxira nusxasini oʻchirdingiz" + "Siz oflayn bo‘lganingizda ham kalitlaringiz zaxiralanish jarayonida edi. Tizimdan chiqishdan oldin kalitlaringizning to‘liq zaxiralanishini ta’minlash uchun qayta ulanishingiz zarur." "Kalitlaringiz hamon zaxiralanmoqda" - "Bu qurilmani olib tashlashdan oldin uning tugashini kuting." + "Tizimdan chiqishdan oldin bu jarayon tugashini kuting." "Kalitlaringiz hamon zaxiralanmoqda" - "Bu qurilmani olib tashlash" - "Bu sizning yagona qurilmangiz. Agar uni o‘chirsangiz, keyingi safar tizimga kirganingizda raqamli shaxsingizni tasdiqlash va shifrlangan chatlaringizni tiklash uchun tiklash kaliti kerak bo‘ladi." + "Tizimdan chiqish" + "Siz oxirgi sessiyangizdan chiqmoqdasiz. Agar hozir chiqib ketsangiz, shifrlangan xabarlaringizga kira olmaysiz." "Qayta tiklash sozlanmagan" - "Bu sizning yagona qurilmangiz. Agar uni olib tashlasangiz, keyingi safar hisobingizga kirganingizda raqamli shaxsingizni tasdiqlash va shifrlangan chatlaringizni tiklash uchun zaxira kaliti kerak bo‘ladi." - "Bu qurilmani olib tashlashdan oldin zaxira kalitiga ruxsatingiz borligini tekshiring" + "Siz oxirgi sessiyangizdan chiqmoqdasiz. Agar hozir chiqib ketsangiz, shifrlangan xabarlaringizga kira olmay qolishingiz mumkin." diff --git a/features/logout/impl/src/main/res/values-zh-rTW/translations.xml b/features/logout/impl/src/main/res/values-zh-rTW/translations.xml index e2b71b61df..12d5bc20a6 100644 --- a/features/logout/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/logout/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,18 +1,17 @@ - "您確定要移除此裝置嗎?" - "移除此裝置" - "移除此裝置" - "正在移除裝置……" - "這是您唯一的裝置。若您移除它,下次登入時您將需要還原金鑰來確認您的數位身份並還原您的加密聊天。" - "您即將失去對您加密聊天的存取權" - "當您離線時,您的金鑰仍在備份中。請重新連線才能在您移除此裝置前備份金鑰。" + "您確定要登出嗎?" + "登出" + "登出" + "正在登出…" + "您將要登出上一次作業階段。若您現在登出,將會失去對加密訊息的存取權。" + "您已關閉備份" + "當您離線時,您的金鑰仍在備份中。請重新連線才能在您登出前備份金鑰。" "您的金鑰仍在備份中" - "請等待此動作完成後再移除此裝置。" + "請等待此動作完成後再登出。" "您的金鑰仍在備份中" - "移除此裝置" - "這是您唯一的裝置。若您移除它,下此登入時將需要還原金鑰來驗證您的數位身份並還原您的加密聊天。" - "您即將失去對您的加密聊天的存取權" - "這是您唯一的裝置。若您移除它,下此登入時將需要還原金鑰來驗證您的數位身份並還原您的加密聊天。" - "在移除此裝置前,請確保您可存取您的還原金鑰" + "登出" + "您將要登出上一次作業階段。若您現在登出,將會失去對加密訊息的存取權。" + "未設定復原金鑰" + "您將要登出上一次作業階段。若您現在登出,將會失去對加密訊息的存取權。" 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 f18904d03b..d438c64ff3 100644 --- a/features/logout/impl/src/main/res/values-zh/translations.xml +++ b/features/logout/impl/src/main/res/values-zh/translations.xml @@ -1,18 +1,18 @@ - "你确定要移除此设备?" - "移除此设备" - "移除此设备" - "正在移除设备…" - "这是你的唯一设备。一旦移除,下次登录时你需要使用恢复密钥验证数字身份并恢复加密聊天。" - "你即将失去加密聊天的访问权" - "当你离线时,密钥仍在备份。重新连接以便在移除设备之前备份密钥。" - "你的密钥仍在备份中" - "请等待此操作完成再移除此设备。" - "你的密钥仍在备份中" - "移除此设备" - "这是你的唯一设备。一旦移除,下次登录时你需要使用恢复密钥验证数字身份并恢复加密聊天。" - "你即将失去加密聊天的访问权" - "这是你的唯一设备。一旦移除,下次登录时你需要使用恢复密钥验证数字身份并恢复加密聊天。" - "确保你移除此设备前拥有恢复密钥" + "您确定要删除此设备吗?" + "删除此设备" + "删除此设备" + "正在删除设备……" + "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。" + "您已关闭备份" + "当你离线时,密钥仍在备份中。重新连接以便在登出之前备份密钥。" + "您的密钥仍在备份中" + "请等待此操作完成后再登出。" + "您的密钥仍在备份中" + "删除此设备" + "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。" + "未设置恢复" + "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。" + "您保存了恢复密钥吗?" 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 a42fd891d4..84ca038d7b 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.testtags.TestTags @@ -24,93 +21,97 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LogoutViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on logout sends a LogoutEvents`() = runAndroidComposeUiTest { + fun `clicking on logout sends a LogoutEvents`() { val eventsRecorder = EventsRecorder() - setLogoutView( + rule.setLogoutView( aLogoutState( eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_signout) + rule.clickOn(CommonStrings.action_signout) eventsRecorder.assertSingle(LogoutEvents.Logout(false)) } @Test - fun `confirming logout sends a LogoutEvents`() = runAndroidComposeUiTest { + fun `confirming logout sends a LogoutEvents`() { val eventsRecorder = EventsRecorder() - setLogoutView( + rule.setLogoutView( aLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - pressTag(TestTags.dialogPositive.value) + rule.pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(LogoutEvents.Logout(false)) } @Test - fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { + fun `clicking on back invoke back callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setLogoutView( + rule.setLogoutView( aLogoutState( eventSink = eventsRecorder ), onBackClick = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on confirm after error sends a LogoutEvents`() = runAndroidComposeUiTest { + fun `clicking on confirm after error sends a LogoutEvents`() { val eventsRecorder = EventsRecorder() - setLogoutView( + rule.setLogoutView( aLogoutState( logoutAction = AsyncAction.Failure(Exception("Failed to logout")), eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_signout_anyway) + rule.clickOn(CommonStrings.action_signout_anyway) eventsRecorder.assertSingle(LogoutEvents.Logout(true)) } @Test - fun `clicking on cancel after error sends a LogoutEvents`() = runAndroidComposeUiTest { + fun `clicking on cancel after error sends a LogoutEvents`() { val eventsRecorder = EventsRecorder() - setLogoutView( + rule.setLogoutView( aLogoutState( logoutAction = AsyncAction.Failure(Exception("Failed to logout")), eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(LogoutEvents.CloseDialogs) } @Test - fun `last session setting button invoke onChangeRecoveryKeyClicked`() = runAndroidComposeUiTest { + fun `last session setting button invoke onChangeRecoveryKeyClicked`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setLogoutView( + rule.setLogoutView( aLogoutState( isLastDevice = true, eventSink = eventsRecorder ), onChangeRecoveryKeyClick = callback, ) - clickOn(CommonStrings.common_settings) + rule.clickOn(CommonStrings.common_settings) } } } -private fun AndroidComposeUiTest.setLogoutView( +private fun AndroidComposeTestRule.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 99860259c4..8eae534740 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutState @@ -24,79 +21,83 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBackKey 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() + @Test - fun `clicking on confirm logout sends expected Event`() = runAndroidComposeUiTest { + fun `clicking on confirm logout sends expected Event`() { val eventsRecorder = EventsRecorder() - setDefaultDirectLogoutView( + rule.setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder, ) ) - clickOn(CommonStrings.action_signout) + rule.clickOn(CommonStrings.action_signout) eventsRecorder.assertSingle(DirectLogoutEvents.Logout(false)) } @Test - fun `clicking on cancel logout sends expected Event`() = runAndroidComposeUiTest { + fun `clicking on cancel logout sends expected Event`() { val eventsRecorder = EventsRecorder() - setDefaultDirectLogoutView( + rule.setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder, ) ) - clickOn(CommonStrings.action_cancel) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on back invoke back callback`() { val eventsRecorder = EventsRecorder() - setDefaultDirectLogoutView( + rule.setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder, ) ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) } @Test - fun `clicking on confirm after error sends expected Event`() = runAndroidComposeUiTest { + fun `clicking on confirm after error sends expected Event`() { val eventsRecorder = EventsRecorder() - setDefaultDirectLogoutView( + rule.setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ) ) - clickOn(CommonStrings.action_signout_anyway) + rule.clickOn(CommonStrings.action_signout_anyway) eventsRecorder.assertSingle(DirectLogoutEvents.Logout(true)) } @Test - fun `clicking on cancel after error sends expected Event`() = runAndroidComposeUiTest { + fun `clicking on cancel after error sends expected Event`() { val eventsRecorder = EventsRecorder() - setDefaultDirectLogoutView( + rule.setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ) ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) } } -private fun AndroidComposeUiTest.setDefaultDirectLogoutView( +private fun AndroidComposeTestRule.setDefaultDirectLogoutView( state: DirectLogoutState, ) { setContent { diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 2661f7e330..d3455fa487 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -62,6 +62,7 @@ dependencies { implementation(projects.libraries.uiUtils) implementation(projects.libraries.testtags) implementation(projects.features.networkmonitor.api) + implementation(projects.features.wallet.impl) implementation(projects.services.analytics.compose) implementation(projects.services.appnavstate.api) implementation(projects.services.toolbox.api) @@ -70,7 +71,6 @@ 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 4d621e417f..bef8ca84d6 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,8 +18,6 @@ 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 d20dc4b38e..a268334ef4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -24,7 +24,7 @@ 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.CallData +import io.element.android.features.call.api.CallType 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 @@ -53,6 +53,9 @@ 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.features.wallet.api.WalletEntryPoint +import io.element.android.features.wallet.impl.panel.WalletPanelNode +import io.element.android.features.wallet.impl.setup.WalletSetupNode import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.callback @@ -106,6 +109,7 @@ class MessagesFlowNode( private val shareLocationEntryPoint: ShareLocationEntryPoint, private val showLocationEntryPoint: ShowLocationEntryPoint, private val createPollEntryPoint: CreatePollEntryPoint, + private val walletEntryPoint: WalletEntryPoint, private val elementCallEntryPoint: ElementCallEntryPoint, private val mediaViewerEntryPoint: MediaViewerEntryPoint, private val forwardEntryPoint: ForwardEntryPoint, @@ -143,7 +147,6 @@ class MessagesFlowNode( val mediaInfo: MediaInfo, val mediaSource: MediaSource, val thumbnailSource: MediaSource?, - val canUseOverlay: Boolean, ) : NavTarget @Parcelize @@ -182,6 +185,20 @@ class MessagesFlowNode( @Parcelize data class Thread(val threadRootId: ThreadId, val focusedEventId: EventId?) : NavTarget + @Parcelize + data object WalletPanel : NavTarget + + @Parcelize + data object WalletSetup : NavTarget + + @Parcelize + data class PaymentFlow( + val roomId: RoomId, + val recipientUserId: UserId?, + val recipientAddress: String?, + val amountLovelace: Long?, + ) : NavTarget + @Parcelize data object ThreadsList : NavTarget } @@ -228,11 +245,10 @@ class MessagesFlowNode( callback.navigateToRoomDetails() } - override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean { + override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean { return processEventClick( timelineMode = timelineMode, event = event, - canUseOverlay = canUseOverlay, ) } @@ -278,18 +294,14 @@ class MessagesFlowNode( backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId)) } - override fun navigateToCurrentLiveLocation() { - backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId))) - } - override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) { - val callData = CallData( + val callType = CallType.RoomCall( sessionId = sessionId, roomId = roomId, - isAudioCall = isAudioCall, + isAudioCall = isAudioCall ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - elementCallEntryPoint.startCall(callData) + elementCallEntryPoint.startCall(callType) } override fun navigateToPinnedMessagesList() { @@ -304,6 +316,19 @@ class MessagesFlowNode( backstack.push(NavTarget.Thread(threadRootId, focusedEventId)) } + override fun navigateToWallet() { + backstack.push(NavTarget.WalletPanel) + } + + override fun navigateToPaymentFlow( + roomId: RoomId, + recipientUserId: UserId?, + recipientAddress: String?, + amountLovelace: Long?, + ) { + backstack.push(NavTarget.PaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace)) + } + override fun navigateToThreadsList() { backstack.push(NavTarget.ThreadsList) } @@ -326,11 +351,7 @@ class MessagesFlowNode( ) val callback = object : MediaViewerEntryPoint.Callback { override fun onDone() { - if (navTarget.canUseOverlay) { - overlay.hide() - } else { - backstack.pop() - } + overlay.hide() } override fun viewInTimeline(eventId: EventId) { @@ -424,11 +445,10 @@ class MessagesFlowNode( } NavTarget.PinnedMessagesList -> { val callback = object : PinnedMessagesListNode.Callback { - override fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean) { + override fun handleEventClick(event: TimelineItem.Event) { processEventClick( timelineMode = Timeline.Mode.PinnedEvents, event = event, - canUseOverlay = canUseOverlay, ) } @@ -467,11 +487,10 @@ class MessagesFlowNode( focusedEventId = navTarget.focusedEventId, ) val callback = object : ThreadedMessagesNode.Callback { - override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean { + override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean { return processEventClick( timelineMode = timelineMode, event = event, - canUseOverlay = canUseOverlay, ) } @@ -517,30 +536,91 @@ class MessagesFlowNode( backstack.push(NavTarget.EditPoll(Timeline.Mode.Thread(navTarget.threadRootId), eventId)) } - override fun navigateToCurrentLiveLocation() { - backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId))) - } - override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) { - val callData = CallData( + val callType = CallType.RoomCall( sessionId = sessionId, roomId = roomId, isAudioCall = isAudioCall ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - elementCallEntryPoint.startCall(callData) + elementCallEntryPoint.startCall(callType) } override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) { backstack.push(NavTarget.Thread(threadRootId, focusedEventId)) } + override fun navigateToPaymentFlow( + roomId: RoomId, + recipientUserId: UserId?, + recipientAddress: String?, + amountLovelace: Long?, + ) { + backstack.push(NavTarget.PaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace)) + } + override fun navigateToDeveloperSettings() { callback.navigateToDeveloperSettings() } } createNode(buildContext, listOf(inputs, callback)) } + is NavTarget.WalletPanel -> { + val walletPanelCallback = object : WalletPanelNode.Callback { + override fun onClose() { + backstack.pop() + } + + override fun onSendAda() { + backstack.pop() + backstack.push(NavTarget.PaymentFlow(room.roomId, null, null, null)) + } + + override fun onSetupWallet() { + backstack.push(NavTarget.WalletSetup) + } + } + createNode(buildContext, listOf(walletPanelCallback)) + } + is NavTarget.WalletSetup -> { + val setupCallback = object : WalletSetupNode.Callback { + override fun onSetupComplete() { + // Pop setup, stay on wallet panel which will now show the wallet + backstack.pop() + } + + override fun onBack() { + backstack.pop() + } + } + createNode(buildContext, listOf(setupCallback)) + } + is NavTarget.PaymentFlow -> { + val walletCallback = object : WalletEntryPoint.Callback { + override fun onPaymentSent(txHash: String) { + backstack.pop() + } + + override fun onPaymentCancelled() { + backstack.pop() + } + + override fun onOpenWalletSettings() { + backstack.pop() + backstack.push(NavTarget.WalletPanel) + } + } + walletEntryPoint.paymentFlowBuilder( + parentNode = this, + buildContext = buildContext, + callback = walletCallback, + ) + .setRoomId(navTarget.roomId) + .setRecipientUserId(navTarget.recipientUserId) + .setRecipientAddress(navTarget.recipientAddress) + .setAmount(navTarget.amountLovelace?.toString()) + .build() + } NavTarget.ThreadsList -> { val callback = object : ThreadsListNode.Callback { override fun openThread(threadId: ThreadId) { @@ -563,7 +643,6 @@ class MessagesFlowNode( private fun processEventClick( timelineMode: Timeline.Mode, event: TimelineItem.Event, - canUseOverlay: Boolean, ): Boolean { val navTarget = when (event.content) { is TimelineItemImageContent -> { @@ -573,7 +652,6 @@ class MessagesFlowNode( content = event.content, mediaSource = event.content.mediaSource, thumbnailSource = event.content.thumbnailSource, - canUseOverlay = canUseOverlay, ) } is TimelineItemVideoContent -> { @@ -583,7 +661,6 @@ class MessagesFlowNode( content = event.content, mediaSource = event.content.mediaSource, thumbnailSource = event.content.thumbnailSource, - canUseOverlay = canUseOverlay, ) } is TimelineItemFileContent -> { @@ -593,7 +670,6 @@ class MessagesFlowNode( content = event.content, mediaSource = event.content.mediaSource, thumbnailSource = event.content.thumbnailSource, - canUseOverlay = canUseOverlay, ) } is TimelineItemAudioContent -> { @@ -603,32 +679,26 @@ 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, - ) - } - NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() } + val mode = ShowLocationMode.Static( + location = event.content.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 -> null } return when (navTarget) { is NavTarget.MediaViewer -> { - if (canUseOverlay) { - overlay.show(navTarget) - } else { - backstack.push(navTarget) - } + overlay.show(navTarget) true } is NavTarget.LocationViewer -> { @@ -645,7 +715,6 @@ class MessagesFlowNode( content: TimelineItemEventContentWithAttachment, mediaSource: MediaSource, thumbnailSource: MediaSource?, - canUseOverlay: Boolean, ): NavTarget { return NavTarget.MediaViewer( mode = mode, @@ -654,7 +723,6 @@ class MessagesFlowNode( filename = content.filename, fileSize = content.fileSize, caption = content.caption, - formattedCaption = content.formattedCaption, mimeType = content.mimeType, formattedFileSize = content.formattedFileSize, fileExtension = content.fileExtension, @@ -674,7 +742,6 @@ class MessagesFlowNode( ), mediaSource = mediaSource, thumbnailSource = thumbnailSource, - canUseOverlay = canUseOverlay, ) } 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 6113b68aab..202affad57 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt @@ -26,6 +26,21 @@ interface MessagesNavigator { fun navigateToMember(userId: UserId) fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) fun navigateToDeveloperSettings() - fun navigateToCurrentLiveLocation() + + /** + * Navigate to the payment flow for /pay slash command. + * + * @param roomId The current room ID + * @param recipientUserId Optional Matrix user ID recipient + * @param recipientAddress Optional Cardano address recipient + * @param amountLovelace Optional amount in lovelace + */ + fun navigateToPaymentFlow( + roomId: RoomId, + recipientUserId: UserId? = null, + recipientAddress: String? = null, + amountLovelace: Long? = null, + ) + 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 a9ce2f5ba1..8b338e78ff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -68,8 +68,6 @@ 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.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 @@ -117,7 +115,7 @@ class MessagesNode( ) interface Callback : Plugin { - fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean + fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean fun navigateToPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) fun navigateToRoomMemberDetails(userId: UserId) fun handlePermalinkClick(data: PermalinkData) @@ -127,15 +125,15 @@ 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() fun navigateToPinnedMessagesList() fun navigateToKnockRequestsList() fun navigateToDeveloperSettings() - fun navigateToThreadsList() + fun navigateToWallet() + fun navigateToPaymentFlow(roomId: RoomId, recipientUserId: UserId?, recipientAddress: String?, amountLovelace: Long?) } override fun onBuilt() { @@ -240,8 +238,13 @@ class MessagesNode( callback.navigateToDeveloperSettings() } - override fun navigateToCurrentLiveLocation() { - callback.navigateToCurrentLiveLocation() + override fun navigateToPaymentFlow( + roomId: RoomId, + recipientUserId: UserId?, + recipientAddress: String?, + amountLovelace: Long?, + ) { + callback.navigateToPaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace) } private fun displaySameRoomToast() { @@ -254,7 +257,6 @@ 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, ) { @@ -276,11 +278,11 @@ class MessagesNode( onRoomDetailsClick = callback::navigateToRoomDetails, onEventContentClick = { isLive, event -> if (isLive) { - callback.handleEventClick(timelineController.mainTimelineMode(), event, canUseOverlay) + callback.handleEventClick(timelineController.mainTimelineMode(), event) } else { val detachedTimelineMode = timelineController.detachedTimelineMode() if (detachedTimelineMode != null) { - callback.handleEventClick(detachedTimelineMode, event, canUseOverlay) + callback.handleEventClick(detachedTimelineMode, event) } else { false } @@ -302,6 +304,7 @@ class MessagesNode( callback.navigateToRoomCall(room.roomId, isAudioCall) }, onViewAllPinnedMessagesClick = callback::navigateToPinnedMessagesList, + onWalletClick = callback::navigateToWallet, modifier = modifier, knockRequestsBannerView = { knockRequestsBannerRenderer.View( 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 6754d703a3..e5ef451733 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -27,8 +27,6 @@ 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 @@ -79,8 +77,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 @@ -128,7 +126,6 @@ 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 @@ -175,7 +172,6 @@ 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() @@ -221,10 +217,12 @@ class MessagesPresenter( val dmRoomMember by room.getDirectRoomMember(membersState) val roomMemberIdentityStateChanges = identityChangeState.roomMemberIdentityStateChanges + val isKeyShareOnInviteEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false) // The top bar should show a "history" icon if: + // * History sharing is enabled, // * The room is encrypted, and: // * The room's history_visibility allows future users to see content. - val topBarSharedHistoryIcon = roomInfo.sharedHistoryIcon() + val topBarSharedHistoryIcon = if (isKeyShareOnInviteEnabled) roomInfo.sharedHistoryIcon() else SharedHistoryIcon.NONE LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) { if (roomInfo.isEncrypted == true) { @@ -264,18 +262,6 @@ 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 { @@ -288,8 +274,6 @@ class MessagesPresenter( } } navigator.close() - }.invokeOnCompletion { - markingAsReadAndExiting.set(false) } } } @@ -321,13 +305,13 @@ class MessagesPresenter( dmUserVerificationState = dmUserVerificationState, roomMemberModerationState = roomMemberModerationState, topBarSharedHistoryIcon = topBarSharedHistoryIcon, + isDmRoom = roomInfo.isDm, successorRoom = roomInfo.successorRoom, threads = Threads( hasThreads = canOpenThreadList && threadsList.isNotEmpty(), // TODO calculate this properly based on the thread list and the read state of each thread hasUnreadThreads = false, ), - showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom && timelineState.timelineMode !is Timeline.Mode.Thread, eventSink = ::handleEvent, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index a16485c6f7..7d004bfa5b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -56,9 +56,9 @@ data class MessagesState( val roomMemberModerationState: RoomMemberModerationState, /** Type of "shared history" icon to show in the top bar. */ val topBarSharedHistoryIcon: SharedHistoryIcon, + val isDmRoom: Boolean, 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 6389089e07..b11ca17d73 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -44,7 +44,6 @@ 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 @@ -80,7 +79,6 @@ open class MessagesStateProvider : PreviewParameterProvider { currentPinnedMessageIndex = 0, ), ), - aMessagesState(isCurrentlySharingLiveLocationInRoom = true), aMessagesState(successorRoom = SuccessorRoom(RoomId("!id:domain"), null)), aMessagesState( timelineState = aTimelineState( @@ -96,8 +94,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), @@ -123,12 +121,12 @@ fun aMessagesState( dmUserVerificationState: IdentityState? = null, roomMemberModerationState: RoomMemberModerationState = aRoomMemberModerationState(), topBarSharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE, + isDmRoom: Boolean = false, successorRoom: SuccessorRoom? = null, threads: MessagesState.Threads = MessagesState.Threads( hasThreads = false, hasUnreadThreads = false, ), - isCurrentlySharingLiveLocationInRoom: Boolean = false, eventSink: (MessagesEvent) -> Unit = {}, ) = MessagesState( roomId = RoomId("!id:domain"), @@ -156,9 +154,9 @@ fun aMessagesState( dmUserVerificationState = dmUserVerificationState, roomMemberModerationState = roomMemberModerationState, topBarSharedHistoryIcon = topBarSharedHistoryIcon, + isDmRoom = isDmRoom, 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 3e6f14e805..ba90be34b1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -56,7 +56,6 @@ 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 @@ -107,6 +106,7 @@ import io.element.android.libraries.designsystem.text.toAnnotatedString import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.HideKeyboardWhenDisposed @@ -140,6 +140,7 @@ fun MessagesView( onSendLocationClick: () -> Unit, onCreatePollClick: () -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit, + onWalletClick: () -> Unit, onViewAllPinnedMessagesClick: () -> Unit, onThreadsListClick: () -> Unit, modifier: Modifier = Modifier, @@ -206,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, @@ -242,7 +243,9 @@ fun MessagesView( displayThreads = state.timelineState.timelineMode !is Timeline.Mode.Thread && state.threads.hasThreads, roomCallState = state.roomCallState, onJoinCallClick = onJoinCallClick, - onThreadsListClick = onThreadsListClick + onThreadsListClick = onThreadsListClick, + isDmRoom = state.isDmRoom, + onWalletClick = onWalletClick, ) } ) @@ -251,8 +254,8 @@ fun MessagesView( content = { padding -> Box( modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) { MessagesViewContent( state = state, @@ -283,16 +286,18 @@ fun MessagesView( state.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Reply, targetEvent)) }, forceJumpToBottomVisibility = forceJumpToBottomVisibility, + onJoinCallClick = onJoinCallClick, + onWalletClick = onWalletClick, onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, knockRequestsBannerView = knockRequestsBannerView, ) 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, @@ -417,6 +422,8 @@ internal fun MessagesMenuActions( roomCallState: RoomCallState, onJoinCallClick: (isAudioCall: Boolean) -> Unit, onThreadsListClick: () -> Unit, + isDmRoom: Boolean = false, + onWalletClick: (() -> Unit)? = null, ) { if (displayThreads) { Icon( @@ -430,6 +437,16 @@ internal fun MessagesMenuActions( roomCallState = roomCallState, onJoinCallClick = onJoinCallClick, ) + // Wallet button - only show in DM rooms + if (isDmRoom && onWalletClick != null) { + Spacer(Modifier.width(8.dp)) + IconButton(onClick = onWalletClick) { + Icon( + imageVector = CompoundIcons.Chart(), + contentDescription = "Cardano Wallet", + ) + } + } Spacer(Modifier.width(8.dp)) } @@ -460,6 +477,8 @@ private fun MessagesViewContent( onMessageLongClick: (TimelineItem.Event) -> Unit, onSendLocationClick: () -> Unit, onCreatePollClick: () -> Unit, + onJoinCallClick: (isAudioCall: Boolean) -> Unit, + onWalletClick: () -> Unit, onViewAllPinnedMessagesClick: () -> Unit, forceJumpToBottomVisibility: Boolean, onSwipeToReply: (TimelineItem.Event) -> Unit, @@ -468,9 +487,9 @@ private fun MessagesViewContent( ) { Box( modifier = modifier - .fillMaxSize() - .navigationBarsPadding() - .imePadding(), + .fillMaxSize() + .navigationBarsPadding() + .imePadding(), ) { AttachmentsBottomSheet( state = state.composerState, @@ -516,39 +535,31 @@ private fun MessagesViewContent( onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, forceJumpToBottomVisibility = forceJumpToBottomVisibility, + onJoinCallClick = onJoinCallClick, nestedScrollConnection = scrollBehavior.nestedScrollConnection, floatingDateTopOffset = pinnedBannerHeightDp, ) if (state.timelineState.timelineMode !is Timeline.Mode.Thread) { - 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) } + 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, + ) } + knockRequestsBannerView() } - - knockRequestsBannerView() } } } @@ -597,9 +608,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 ) { @@ -641,6 +652,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onSendLocationClick = {}, onCreatePollClick = {}, onJoinCallClick = {}, + onWalletClick = {}, onViewAllPinnedMessagesClick = { }, forceJumpToBottomVisibility = true, knockRequestsBannerView = {}, @@ -696,6 +708,7 @@ internal fun MessagesViewA11yPreview() = ElementPreview { onSendLocationClick = {}, onCreatePollClick = {}, onJoinCallClick = {}, + onWalletClick = {}, onViewAllPinnedMessagesClick = {}, onThreadsListClick = {}, forceJumpToBottomVisibility = true, 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 56bac5be33..a69c6d7612 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,7 +15,6 @@ 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 @@ -128,7 +127,7 @@ open class ActionListStateProvider : PreviewParameterProvider { anActionListState( target = ActionListState.Target.Success( event = aTimelineItemEvent( - content = aTimelineItemLocationContent(mode = aStaticLocationMode()), + content = aTimelineItemLocationContent(), timelineItemReactions = reactionsState ), sentTimeFull = "January 1, 1970 at 12:00 AM", @@ -141,7 +140,7 @@ open class ActionListStateProvider : PreviewParameterProvider { anActionListState( target = ActionListState.Target.Success( event = aTimelineItemEvent( - content = aTimelineItemLocationContent(mode = aStaticLocationMode()), + content = aTimelineItemLocationContent(), 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..d218f32a63 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 @@ -78,6 +78,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPaymentContentWrapper import io.element.android.features.messages.impl.utils.messagesummary.DefaultMessageSummaryFormatter import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -156,7 +157,6 @@ fun ActionListView( sheetState = sheetState, onDismissRequest = ::onDismiss, modifier = modifier, - scrollable = false, ) { ActionListViewContent( state = state, @@ -290,11 +290,7 @@ private fun MessageSummary( is TimelineItemRedactedContent, is TimelineItemUnknownContent -> content = { ContentForBody(textContent) } is TimelineItemLocationContent -> { - val body = when (event.content.mode) { - is TimelineItemLocationContent.Mode.Live -> stringResource(CommonStrings.common_shared_live_location) - is TimelineItemLocationContent.Mode.Static -> stringResource(CommonStrings.common_shared_location) - } - content = { ContentForBody(body) } + content = { ContentForBody(stringResource(CommonStrings.common_shared_location)) } } is TimelineItemImageContent -> { content = { ContentForBody(event.content.bestDescription) } @@ -323,6 +319,9 @@ private fun MessageSummary( is TimelineItemRtcNotificationContent -> { content = { ContentForBody(stringResource(CommonStrings.common_call_started)) } } + is TimelineItemPaymentContentWrapper -> { + content = { ContentForBody(textContent) } + } } Row(modifier = modifier) { icon() 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 73fa55228d..d989b34ab3 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,12 +16,5 @@ import kotlinx.parcelize.Parcelize @Immutable sealed interface Attachment : Parcelable { @Parcelize - 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 + data class Media(val localMedia: LocalMedia) : 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 deleted file mode 100644 index 7feeff18dd..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentMediaInfo.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 a7c64845d1..d473d4c3f4 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,17 +8,8 @@ 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 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 cab00d99c1..8e92e53f6a 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,12 +22,7 @@ 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 @@ -35,6 +30,7 @@ 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 @@ -53,9 +49,7 @@ 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( @@ -66,9 +60,7 @@ 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, @@ -92,14 +84,6 @@ 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( @@ -110,12 +94,9 @@ class AttachmentsPreviewPresenter( var preprocessMediaJob by remember { mutableStateOf(null) } - val mediaAttachment = currentAttachment as Attachment.Media + val mediaAttachment = attachment as Attachment.Media val mediaOptimizationSelectorPresenter = remember { - mediaOptimizationSelectorPresenterFactory.create( - localMedia = mediaAttachment.localMedia, - sendAsFile = mediaAttachment.sendAsFile, - ) + mediaOptimizationSelectorPresenterFactory.create(mediaAttachment.localMedia) } val mediaOptimizationSelectorState by rememberUpdatedState(mediaOptimizationSelectorPresenter.present()) @@ -123,46 +104,24 @@ class AttachmentsPreviewPresenter( var displayFileTooLargeError by remember { mutableStateOf(false) } - LaunchedEffect( - mediaOptimizationSelectorState.displayMediaSelectorViews, - mediaOptimizationSelectorState.videoSizeEstimations, - currentAttachment, - imageEditorState, - isApplyingImageEdits, - ) { + LaunchedEffect(mediaOptimizationSelectorState.displayMediaSelectorViews) { // 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. - @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, + if (mediaOptimizationSelectorState.displayMediaSelectorViews == false) { + preprocessMediaJob = preProcessAttachment( + attachment = attachment, + mediaOptimizationConfig = mediaOptimizationConfigProvider.get(), 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.isImageAttachment() + val isImageFile = mediaAttachment.localMedia.info.mimeType.isMimeTypeImage() 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. @@ -193,7 +152,7 @@ class AttachmentsPreviewPresenter( videoCompressionPreset = mediaOptimizationSelectorState.selectedVideoPreset ?: VideoCompressionPreset.STANDARD, ) preprocessMediaJob = preProcessAttachment( - attachment = currentAttachment, + attachment = attachment, mediaOptimizationConfig = config, displayProgress = true, sendActionState = sendActionState, @@ -212,9 +171,6 @@ 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() @@ -222,36 +178,33 @@ 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) { - 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() - } + sendPreProcessedMedia( + mediaUploadInfo = mediaUploadInfo, + caption = caption, + sendActionState = sendActionState, + dismissAfterSend = false, + inReplyToEventId = inReplyToEventId, + ) + + // 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(sendActionState, editedTempFile) + dismiss( + attachment, + sendActionState, + ) } AttachmentsPreviewEvent.CancelAndClearSendState -> { // Cancel media sending @@ -267,88 +220,11 @@ 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.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 = currentAttachment, - imageEditorState = imageEditorState, - canEditImage = canEditImage, - isApplyingImageEdits = isApplyingImageEdits, - displayImageEditError = displayImageEditError, + attachment = attachment, sendActionState = sendActionState.value, textEditorState = textEditorState, mediaOptimizationSelectorState = mediaOptimizationSelectorState, @@ -357,28 +233,6 @@ 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, @@ -425,8 +279,8 @@ class AttachmentsPreviewPresenter( } private fun dismiss( + attachment: Attachment, sendActionState: MutableState, - editedTempFile: File?, ) { // Delete the temporary file when (attachment) { @@ -437,7 +291,6 @@ class AttachmentsPreviewPresenter( } } } - editedTempFile?.safeDelete() // Reset the sendActionState to ensure that dialog is closed before the screen sendActionState.value = SendActionState.Done onDoneListener() @@ -451,12 +304,6 @@ 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 463479fe55..97ca230d77 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,17 +10,12 @@ 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 a2df440a12..70d7ab006e 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,8 +11,6 @@ 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 @@ -44,9 +42,6 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider false - is SendActionState.Sending.Processing -> !state.sendActionState.displayProgress - SendActionState.Done -> false - else -> true - } - fun postSendAttachment() { state.eventSink(AttachmentsPreviewEvent.SendAttachment) } @@ -105,84 +93,33 @@ fun AttachmentsPreviewView( state.eventSink(AttachmentsPreviewEvent.CancelAndClearSendState) } - fun postOpenImageEditor() { - state.eventSink(AttachmentsPreviewEvent.OpenImageEditor) - } - - 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() - } + postCancel() } - if (state.imageEditorState != null) { - AttachmentImageEditorView( - state = state.imageEditorState, - onCropRectChange = { cropRect -> - state.eventSink(AttachmentsPreviewEvent.UpdateImageCropRect(cropRect)) - }, - onRotateClick = { state.eventSink(AttachmentsPreviewEvent.RotateImageToTheLeft) }, - 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) { - TextButton( - stringResource(CommonStrings.action_edit), - onClick = ::postOpenImageEditor - ) - } - } - ) - } - ) { paddingValues -> - AttachmentPreviewContent( - modifier = Modifier.padding(paddingValues), - state = state, - localMediaRenderer = localMediaRenderer, - onSendClick = ::postSendAttachment, + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { + BackButton( + imageVector = CompoundIcons.Close(), + onClick = ::postCancel, + ) + }, + title = {}, ) } + ) { 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 ) @@ -191,56 +128,36 @@ fun AttachmentsPreviewView( @Composable private fun AttachmentSendStateView( sendActionState: SendActionState, - isApplyingImageEdits: Boolean, - displayImageEditError: Boolean, - onDismissImageEditError: () -> Unit, onDismissClick: () -> Unit, onRetryClick: () -> Unit ) { - 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 -> { + when (sendActionState) { + is SendActionState.Sending.Processing -> { + if (sendActionState.displayProgress) { ProgressDialog( type = ProgressDialogType.Indeterminate, - text = stringResource(id = CommonStrings.common_sending), + text = stringResource(CommonStrings.common_preparing), 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 } } @@ -267,10 +184,10 @@ private fun AttachmentPreviewContent( } } } - val mediaInfo = (state.attachment as? Attachment.Media)?.localMedia?.info - if (mediaInfo?.isImageAttachment() == true) { + val mimeType = (state.attachment as? Attachment.Media)?.localMedia?.info?.mimeType + if (mimeType?.isMimeTypeImage() == true) { ImageOptimizationSelector(state.mediaOptimizationSelectorState) - } else if (mediaInfo?.mimeType?.isMimeTypeVideo() == true) { + } else if (mimeType?.isMimeTypeVideo() == true) { VideoPresetSelector(state = state.mediaOptimizationSelectorState) } @@ -303,8 +220,7 @@ 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)) @@ -313,9 +229,7 @@ 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, ) @@ -341,8 +255,7 @@ 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 deleted file mode 100644 index cb66802184..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditor.kt +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 rotatedBitmap = normalizedBitmap.rotateQuarterTurns(edits.rotationQuarterTurns) - if (rotatedBitmap !== normalizedBitmap) { - normalizedBitmap.recycle() - } - - val cropRect = edits.cropRect.toPixelRect( - imageWidth = rotatedBitmap.width, - imageHeight = rotatedBitmap.height, - ) - val isCropUnchanged = cropRect.left == 0 && cropRect.top == 0 && - cropRect.width() == rotatedBitmap.width && cropRect.height() == rotatedBitmap.height - val croppedBitmap = if (isCropUnchanged) { - rotatedBitmap - } else { - Bitmap.createBitmap( - rotatedBitmap, - cropRect.left, - cropRect.top, - cropRect.width(), - cropRect.height(), - ) - } - if (croppedBitmap !== rotatedBitmap) { - rotatedBitmap.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.rotateQuarterTurns(quarterTurns: Int): Bitmap { - val normalizedTurns = (quarterTurns % 4 + 4) % 4 - if (normalizedTurns == 0) return this - val matrix = Matrix().apply { - postRotate(normalizedTurns * 90f) - } - 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 deleted file mode 100644 index 3c8af52ce8..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorState.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 normalizedRotationQuarterTurns: Int - get() = rotationQuarterTurns % 4 - - val rotationDegrees: Int - get() = normalizedRotationQuarterTurns * 90 - - val hasChanges: Boolean - get() = cropRect != NormalizedCropRect.default() || normalizedRotationQuarterTurns != 0 - - fun rotateAntiClockwise(): AttachmentImageEdits { - return copy( - rotationQuarterTurns = (normalizedRotationQuarterTurns + 3) % 4, - // Also update the crop rect to keep the same selected area - cropRect = NormalizedCropRect( - left = cropRect.top, - top = 1f - cropRect.right, - right = cropRect.bottom, - bottom = 1f - cropRect.left, - ) - ) - } -} - -@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), - ) - } - - 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 deleted file mode 100644 index df4bb5257f..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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, - ), - ) -} - -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 deleted file mode 100644 index 6011b52c41..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorView.kt +++ /dev/null @@ -1,647 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.messages.impl.attachments.preview.imageeditor - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -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.foundation.shape.CircleShape -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, - 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 rotateButtonBackground = ElementTheme.colors.bgCanvasDefault - - 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, - ) - } - Box( - modifier = Modifier.weight(1f), - contentAlignment = Alignment.Center, - ) { - IconButton( - onClick = onRotateClick, - modifier = Modifier - .background( - color = rotateButtonBackground, - shape = CircleShape, - ) - .border(1.dp, ElementTheme.colors.borderInteractiveSecondary, CircleShape) - .clearAndSetSemantics { - contentDescription = rotateContentDescription - stateDescription = rotationStateDescription - } - ) { - Icon( - modifier = Modifier - .size(22.dp), - imageVector = CompoundIcons.RotateLeft(), - contentDescription = null, - ) - } - } - Box( - modifier = Modifier.weight(1f), - contentAlignment = Alignment.CenterEnd, - ) { - TextButton( - text = stringResource(CommonStrings.action_done), - 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 - - 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 { rotationZ = rotationQuarterTurns * 90f }, - contentScale = ContentScale.Fit, - ) - } else { - AsyncImage( - model = state.localMedia.uri, - contentDescription = stringResource(CommonStrings.common_image), - modifier = Modifier - .requiredSize(imageLayoutWidthDp, imageLayoutHeightDp) - .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() - onCropRectChange( - latestCropRect.applyChange( - dragTarget = activeTarget, - deltaX = dragAmount.x / size.width.toFloat(), - deltaY = dragAmount.y / size.height.toFloat(), - ) - ) - } - }, - 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 = {}, - 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 abc0264b2f..c81c306f90 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,11 +37,9 @@ 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) @@ -49,7 +47,6 @@ class DefaultMediaOptimizationSelectorPresenter( interface Factory : MediaOptimizationSelectorPresenter.Factory { override fun create( localMedia: LocalMedia, - sendAsFile: Boolean, ): DefaultMediaOptimizationSelectorPresenter } @@ -58,9 +55,7 @@ class DefaultMediaOptimizationSelectorPresenter( @Composable override fun present(): MediaOptimizationSelectorState { val displayMediaSelectorViews by produceState(null) { - // 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) + value = featureFlagService.isFeatureEnabled(FeatureFlags.SelectableMediaQuality) } var displayVideoPresetSelectorDialog by remember { mutableStateOf(false) } @@ -128,23 +123,12 @@ 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 = videoCompressionPresetSelector.selectBestVideoPreset( - expectedVideoPreset = mediaOptimizationConfig.videoCompressionPreset, + selectedVideoOptimizationPreset = findBestVideoPreset( + defaultVideoPreset = mediaOptimizationConfig.videoCompressionPreset, videoSizeEstimations = videoSizeEstimations, ) } @@ -192,4 +176,20 @@ 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 f1e17ef0a6..80cdfd9467 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,7 +15,6 @@ 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 b0ff50dc72..a6945b5ebe 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,6 +30,7 @@ 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 be53de5f66..47d1947766 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,7 +11,6 @@ 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 @@ -33,7 +32,7 @@ class IdentityChangeStateProvider : PreviewParameterProvider { override val values: Sequence @@ -38,10 +37,10 @@ fun aResolveVerifiedUserSendFailureState( eventSink = eventSink ) -fun anUnsignedDeviceSendFailure(userDisplayName: String = USER_NAME_ALICE) = VerifiedUserSendFailure.UnsignedDevice.FromOther( +fun anUnsignedDeviceSendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.UnsignedDevice.FromOther( userDisplayName = userDisplayName, ) -fun aChangedIdentitySendFailure(userDisplayName: String = USER_NAME_ALICE) = VerifiedUserSendFailure.ChangedIdentity( +fun aChangedIdentitySendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.ChangedIdentity( userDisplayName = userDisplayName, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt index c14dd03f1e..98e2bba3be 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt @@ -75,7 +75,6 @@ fun ResolveVerifiedUserSendFailureView( .navigationBarsPadding(), sheetState = sheetState, onDismissRequest = ::dismiss, - scrollable = true, ) { IconTitleSubtitleMolecule( modifier = Modifier.padding(24.dp), 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..1fdb61f484 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 @@ -13,8 +13,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -76,8 +74,7 @@ internal fun AttachmentsBottomSheet( sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true ), - onDismissRequest = { isVisible = false }, - scrollable = false, + onDismissRequest = { isVisible = false } ) { AttachmentSourcePickerMenu( state = state, @@ -100,7 +97,6 @@ private fun AttachmentSourcePickerMenu( modifier = Modifier .navigationBarsPadding() .imePadding() - .verticalScroll(rememberScrollState()) ) { ListItem( modifier = Modifier.clickable { state.eventSink(MessageComposerEvent.PickAttachmentSource.PhotoFromCamera) }, 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 e226318345..89ab1d76a3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -36,6 +36,8 @@ import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.Attachment.Media import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError +import io.element.android.features.wallet.impl.slash.ParsedPayCommand +import io.element.android.features.wallet.impl.slash.SlashCommandParser import io.element.android.features.messages.impl.draft.ComposerDraftService import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor @@ -57,6 +59,7 @@ 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 @@ -101,6 +104,7 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber +import io.element.android.libraries.ui.strings.CommonStrings import kotlin.time.Duration.Companion.seconds import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes @@ -131,6 +135,7 @@ class MessageComposerPresenter( private val suggestionsProcessor: SuggestionsProcessor, private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider, private val notificationConversationService: NotificationConversationService, + private val slashCommandParser: SlashCommandParser, private val slashCommandService: SlashCommandService, ) : Presenter { @AssistedFactory @@ -179,7 +184,7 @@ class MessageComposerPresenter( handlePickedMedia(uri, mimeType) } val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType -> - handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream, sendAsFile = true) + handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream) } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri -> handlePickedMedia(uri, MimeTypes.Jpeg) @@ -460,6 +465,55 @@ class MessageComposerPresenter( val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = true) val capturedMode = messageComposerContext.composerMode + // Check for /pay slash command FIRST (our Cardano wallet integration). + // If matched, short-circuit before upstream's slash command service runs. + if (capturedMode is MessageComposerMode.Normal) { + val payCommand = parsePayCommand(message.markdown) + if (payCommand != null) { + when (payCommand) { + is ParsedPayCommand.ParseError -> { + // Show error, keep text in composer + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error)) + return@launch + } + is ParsedPayCommand.WithAddressRecipient -> { + // Reset composer and navigate to payment flow + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) + navigator.navigateToPaymentFlow( + roomId = room.roomId, + recipientAddress = payCommand.address, + amountLovelace = payCommand.amount, + ) + return@launch + } + is ParsedPayCommand.WithMatrixRecipient -> { + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) + navigator.navigateToPaymentFlow( + roomId = room.roomId, + recipientUserId = payCommand.matrixUserId, + amountLovelace = payCommand.amount, + ) + return@launch + } + is ParsedPayCommand.AmountOnly -> { + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) + navigator.navigateToPaymentFlow( + roomId = room.roomId, + amountLovelace = payCommand.amount, + ) + return@launch + } + is ParsedPayCommand.Empty -> { + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) + navigator.navigateToPaymentFlow( + roomId = room.roomId, + ) + return@launch + } + } + } + } + val slashCommand = if (capturedMode is MessageComposerMode.Normal) { slashCommandService.parse( textMessage = message.markdown, @@ -571,7 +625,7 @@ class MessageComposerPresenter( notificationConversationService.onSendMessage( sessionId = room.sessionId, roomId = roomInfo.id, - roomName = roomInfo.name, + roomName = roomInfo.name ?: roomInfo.id.value, roomIsDirect = roomInfo.isDm, roomAvatarUrl = roomInfo.avatarUrl ?: roomMembers.getDirectRoomMember(roomInfo = roomInfo, sessionId = room.sessionId)?.avatarUrl, ) @@ -605,7 +659,6 @@ class MessageComposerPresenter( private fun handlePickedMedia( uri: Uri?, mimeType: String? = null, - sendAsFile: Boolean = false, ) { uri ?: return val localMedia = localMediaFactory.createFromUri( @@ -614,7 +667,7 @@ class MessageComposerPresenter( name = null, formattedFileSize = null ) - val mediaAttachment = Attachment.Media(localMedia, sendAsFile = sendAsFile) + val mediaAttachment = Attachment.Media(localMedia) val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId navigator.navigateToPreviewAttachments(persistentListOf(mediaAttachment), inReplyToEventId) @@ -850,4 +903,14 @@ class MessageComposerPresenter( } } } + + /** + * Parses the message text for /pay slash command. + * + * @param messageText The raw message text + * @return ParsedPayCommand if this is a /pay command, null otherwise + */ + private fun parsePayCommand(messageText: String): ParsedPayCommand? { + return slashCommandParser.parse(messageText) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 245b23cf8e..d387bc8765 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -84,7 +84,6 @@ internal fun MessageComposerView( val onSendVoiceMessage = { voiceMessageState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage) - state.eventSink(MessageComposerEvent.CloseSpecialMode) } val onDeleteVoiceMessage = { 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 ef2362c794..678ef2ba56 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt @@ -33,7 +33,6 @@ 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 @@ -190,7 +189,6 @@ internal fun SuggestionsPickerViewPreview() { isIgnored = false, role = RoomMember.Role.User, membershipChangeReason = null, - isServiceMember = false, ) val anAlias = remember { RoomAlias("#room:domain.org") } SuggestionsPickerView( @@ -200,7 +198,7 @@ internal fun SuggestionsPickerViewPreview() { suggestions = persistentListOf( ResolvedSuggestion.AtRoom, ResolvedSuggestion.Member(roomMember), - ResolvedSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = USER_NAME_BOB)), + ResolvedSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "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/messagecomposer/suggestions/SuggestionsProcessor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt index 010aff5d4b..5fd4a4e067 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.slashcommands.api.SlashCommandService +import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.SuggestionType @@ -77,9 +78,23 @@ class SuggestionsProcessor( SuggestionType.Command -> { // Command suggestions are valid only if this is the beginning of the message if (suggestion.start == 0) { - slashCommandService.getSuggestions(suggestion.text, isInThread).map { + val upstream = slashCommandService.getSuggestions(suggestion.text, isInThread).map { ResolvedSuggestion.Command(it) } + val wallet = if ("pay".startsWith(suggestion.text, ignoreCase = true)) { + listOf( + ResolvedSuggestion.Command( + SlashCommandSuggestion( + command = "pay", + parameters = "[recipient] [amount]", + description = "Send ADA to someone", + ) + ) + ) + } else { + emptyList() + } + upstream + wallet } else { emptyList() } 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 a618117950..cddc1831db 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -38,8 +38,6 @@ 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 @@ -52,7 +50,7 @@ class PinnedMessagesListNode( private val permalinkParser: PermalinkParser, ) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator { interface Callback : Plugin { - fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean) + fun handleEventClick(event: TimelineItem.Event) fun navigateToRoomMemberDetails(userId: UserId) fun viewInTimeline(eventId: EventId) fun handlePermalinkClick(data: PermalinkData.RoomLink) @@ -105,7 +103,6 @@ class PinnedMessagesListNode( @Composable override fun View(modifier: Modifier) { - val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard() CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { @@ -116,9 +113,7 @@ class PinnedMessagesListNode( PinnedMessagesListView( state = state, onBackClick = ::navigateUp, - onEventClick = { - callback.handleEventClick(it, canUseOverlay) - }, + onEventClick = callback::handleEventClick, 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 9e5f5ba304..6cd037484d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -44,6 +44,7 @@ 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.ui.strings.CommonStrings 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 7190aa174f..b212549a22 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -235,6 +235,7 @@ private fun PinnedMessagesListLoaded( onMoreReactionsClick = {}, onReadReceiptClick = {}, onSwipeToReply = {}, + onJoinCallClick = {}, eventSink = { timelineItemEvent -> when (timelineItemEvent) { is TimelineEvent.OpenThread -> state.eventSink(PinnedMessagesListEvent.OpenThread(timelineItemEvent.threadRootEventId)) 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 be573fa92f..0af0fbb9f5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt @@ -67,8 +67,7 @@ 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.ui.utils.a11y.hasExternalKeyboard -import io.element.android.libraries.ui.utils.a11y.isTalkbackActive +import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.collections.immutable.ImmutableList @@ -87,6 +86,7 @@ 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, @@ -124,7 +124,7 @@ class ThreadedMessagesNode( } interface Callback : Plugin { - fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean + fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean fun navigateToPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) fun navigateToRoomMemberDetails(userId: UserId) fun handlePermalinkClick(data: PermalinkData) @@ -134,10 +134,10 @@ 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 navigateToPaymentFlow(roomId: RoomId, recipientUserId: UserId?, recipientAddress: String?, amountLovelace: Long?) } override fun onBuilt() { @@ -155,6 +155,9 @@ class ThreadedMessagesNode( onStop = { appNavigationStateService.onLeavingThread(id) }, + onDestroy = { + mediaPlayer.close() + } ) } @@ -244,9 +247,13 @@ class ThreadedMessagesNode( callback.navigateToDeveloperSettings() } - override fun navigateToCurrentLiveLocation() { - // Shouldn't happen because LiveLocationSharingBanner is not shown in threads. - callback.navigateToCurrentLiveLocation() + override fun navigateToPaymentFlow( + roomId: RoomId, + recipientUserId: UserId?, + recipientAddress: String?, + amountLovelace: Long?, + ) { + callback.navigateToPaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace) } override fun close() = navigateUp() @@ -255,7 +262,6 @@ class ThreadedMessagesNode( override fun View(modifier: Modifier) { val activity = requireNotNull(LocalActivity.current) val isDark = ElementTheme.isLightTheme.not() - val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard() CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { @@ -275,11 +281,11 @@ class ThreadedMessagesNode( onEventContentClick = { isLive, event -> timelineController?.let { controller -> if (isLive) { - callback.handleEventClick(controller.mainTimelineMode(), event, canUseOverlay) + callback.handleEventClick(controller.mainTimelineMode(), event) } else { val detachedTimelineMode = controller.detachedTimelineMode() if (detachedTimelineMode != null) { - callback.handleEventClick(detachedTimelineMode, event, canUseOverlay) + callback.handleEventClick(detachedTimelineMode, event) } else { false } @@ -301,6 +307,7 @@ class ThreadedMessagesNode( onJoinCallClick = { isAudioCall -> callback.navigateToRoomCall(room.roomId, isAudioCall) }, + onWalletClick = {}, onViewAllPinnedMessagesClick = {}, modifier = modifier, knockRequestsBannerView = {}, 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..9d15376e9f 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 @@ -8,10 +8,12 @@ package io.element.android.features.messages.impl.threads.list import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter @@ -95,6 +97,12 @@ class ThreadsListPresenter( val roomInfo by room.roomInfoFlow.collectAsState() + DisposableEffect(Unit) { + onDispose { + threadsListService.destroy() + } + } + fun handleEvent(event: ThreadsListEvents) { when (event) { ThreadsListEvents.Paginate -> if ((paginationStatus as? ThreadListPaginationStatus.Idle)?.hasMoreToLoad == true) { 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 5e26d849a5..c93af5c162 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 @@ -31,9 +31,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.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 @@ -46,8 +43,6 @@ 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 @@ -82,18 +77,7 @@ fun ThreadsListView( topBar = { TopAppBar( title = { - 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 - }, - ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { Avatar( avatarData = AvatarData( id = state.roomId.value, @@ -319,7 +303,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, @@ -376,7 +360,7 @@ fun aThreadListItem( fun aThreadListItemEvent( threadId: ThreadId = ThreadId("\$a-thread-id"), senderId: UserId = UserId("@a-user-id:server"), - senderProfile: ProfileDetails = ProfileDetails.Ready(displayName = USER_NAME_ALICE, displayNameAmbiguous = false, avatarUrl = null), + senderProfile: ProfileDetails = ProfileDetails.Ready(displayName = "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/TimelineEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt index e9a6ce5549..1591cbf6cc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt @@ -57,6 +57,4 @@ sealed interface TimelineEvent { data class EditPoll( val pollStartId: EventId, ) : TimelineItemPollEvent - - data object StopLiveLocationShare : TimelineItemEvent } 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 0681a0ff38..8a7011552e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -23,7 +23,6 @@ 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 @@ -49,6 +48,7 @@ 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,7 +66,6 @@ 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 @@ -95,7 +94,6 @@ class TimelinePresenter( private val roomCallStatePresenter: Presenter, private val featureFlagService: FeatureFlagService, private val analyticsService: AnalyticsService, - private val liveLocationShareManager: ActiveLiveLocationShareManager, ) : Presenter { private val tag = "TimelinePresenter" @@ -138,6 +136,9 @@ 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) @@ -169,15 +170,12 @@ class TimelinePresenter( newEventState.value = NewEventState.None } Timber.tag(tag).d("## sendReadReceiptIfNeeded firstVisibleIndex: ${event.firstIndex}") - sessionCoroutineScope.launch { - val sendPublicReadReceipts = sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first() - sendReadReceiptIfNeeded( - firstVisibleIndex = event.firstIndex, - timelineItems = timelineItems, - lastReadReceiptId = lastReadReceiptId, - readReceiptType = if (sendPublicReadReceipts) ReceiptType.READ else ReceiptType.READ_PRIVATE, - ) - } + sessionCoroutineScope.sendReadReceiptIfNeeded( + firstVisibleIndex = event.firstIndex, + timelineItems = timelineItems, + lastReadReceiptId = lastReadReceiptId, + readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE, + ) } else { newEventState.value = NewEventState.None } @@ -202,9 +200,6 @@ class TimelinePresenter( is TimelineEvent.EditPoll -> { navigator.navigateToEditPoll(event.pollStartId) } - 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) 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 0bf293eca4..9840ac5107 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -29,8 +29,6 @@ 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 @@ -145,7 +143,7 @@ internal fun aTimelineItemEvent( isMine: Boolean = false, isEditable: Boolean = false, canBeRepliedTo: Boolean = false, - senderDisplayName: String = USER_NAME_SENDER, + senderDisplayName: String = "Sender", displayNameAmbiguous: Boolean = false, content: TimelineItemEventContent = aTimelineItemTextContent(), groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, @@ -162,7 +160,7 @@ internal fun aTimelineItemEvent( eventId = eventId, transactionId = transactionId, senderId = UserId("@senderId:domain"), - senderAvatar = AvatarData("@senderId:domain", USER_NAME_SENDER, size = AvatarSize.TimelineSender), + senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender), content = content, reactionsState = timelineItemReactions, readReceiptState = readReceiptState, @@ -255,7 +253,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 41a828abb4..0c5bb28890 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -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.a11y.isTalkbackActive +import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.wysiwyg.link.Link import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest @@ -103,6 +103,7 @@ fun TimelineView( onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit, onMoreReactionsClick: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit, + onJoinCallClick: (isAudioCall: Boolean) -> Unit, modifier: Modifier = Modifier, lazyListState: LazyListState = rememberLazyListState(), forceJumpToBottomVisibility: Boolean = false, @@ -186,6 +187,7 @@ fun TimelineView( onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, onSwipeToReply = onSwipeToReply, + onJoinCallClick = onJoinCallClick, eventSink = state.eventSink, ) } @@ -429,6 +431,7 @@ internal fun TimelineViewPreview( onReactionLongClick = { _, _ -> }, onMoreReactionsClick = {}, onReadReceiptClick = {}, + onJoinCallClick = {}, forceJumpToBottomVisibility = true, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt index f0a11081e1..5f9c3d0364 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt @@ -49,6 +49,7 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview { onReactionLongClick = { _, _ -> }, onMoreReactionsClick = {}, onReadReceiptClick = {}, + onJoinCallClick = {}, forceJumpToBottomVisibility = true, ) } 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 80bd342d01..aa5aaa2075 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 1859e66750..096ab018e2 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,7 +26,6 @@ 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 @@ -163,7 +162,7 @@ internal fun MessageShieldViewPreview() { MessageShield.AuthenticityNotGuaranteed(false), forwarder = UserId("@alice:example.com"), forwarderProfile = ProfileDetails.Ready( - displayName = USER_NAME_ALICE, + displayName = "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 25a53cec2d..21ef7c5b09 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,12 +15,9 @@ 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 @@ -50,39 +47,9 @@ 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, ) { @@ -100,22 +67,36 @@ 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), + modifier = Modifier + .size(15.dp, 18.dp) + .clickable( + enabled = isVerifiedUserSendFailure, + onClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) { + eventSink(TimelineEvent.ComputeVerifiedUserSendFailure(event)) + } ) } if (!isMessageRedacted) { - shield?.let { shield -> + event.messageShield?.let { shield -> Spacer(modifier = Modifier.width(2.dp)) Icon( imageVector = shield.toIcon(), contentDescription = stringResource(id = CommonStrings.a11y_encryption_details), - modifier = Modifier.size(15.dp), + modifier = Modifier + .size(15.dp) + .clickable( + onClickLabel = stringResource(CommonStrings.a11y_view_details), + ) { + eventSink(TimelineEvent.ShowShieldDialog(shield)) + }, 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 61273ff07a..a6ae2d9ee5 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,7 +8,6 @@ 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 @@ -23,32 +22,31 @@ 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.aTimelineRoomInfo 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.features.roomcall.api.RoomCallState +import io.element.android.features.roomcall.api.RoomCallStateProvider +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 import io.element.android.libraries.designsystem.text.toDp -import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun TimelineItemCallNotifyView( - timelineRoomInfo: TimelineRoomInfo, event: TimelineItem.Event, - content: TimelineItemRtcNotificationContent, + roomCallState: RoomCallState, onLongClick: (TimelineItem.Event) -> Unit, + onJoinCallClick: (isAudioCall: Boolean) -> Unit, modifier: Modifier = Modifier ) { Row( @@ -66,82 +64,67 @@ internal fun TimelineItemCallNotifyView( 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, + Avatar( + avatarData = event.senderAvatar, + avatarType = AvatarType.User, ) - - Text( - 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, - ) - } -} - -@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 + Column(modifier = Modifier.weight(1f)) { + Text( + text = event.safeSenderName, + style = ElementTheme.typography.fontBodyLgMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(20.sp.toDp()), + imageVector = 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, + ) + } + } + if (roomCallState is RoomCallState.OnGoing) { + CallMenuItem( + roomCallState = roomCallState, + onJoinCallClick = onJoinCallClick, + ) + } else { + Text( + text = event.sentTime, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } - 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(2.dp), verticalArrangement = Arrangement.spacedBy(2.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), - content = content, - onLongClick = {}, - ) - } + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + RoomCallStateProvider() + .values + .filter { it !is RoomCallState.Unavailable } + .forEach { roomCallState -> + TimelineItemCallNotifyView( + event = aTimelineItemEvent(content = TimelineItemRtcNotificationContent()), + roomCallState = roomCallState, + onLongClick = {}, + onJoinCallClick = {}, + ) } - } } } 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 936028cbd6..0361eebc01 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -78,7 +78,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent -import io.element.android.features.messages.impl.timeline.model.event.ensureActiveLiveLocation import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.mustBeProtected @@ -92,7 +91,6 @@ 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 @@ -121,7 +119,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.a11y.isTalkbackActive +import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.wysiwyg.link.Link import kotlinx.coroutines.launch import kotlin.math.abs @@ -679,7 +677,6 @@ private fun MessageEventBubbleContent( .padding(horizontal = 8.dp, vertical = 4.dp) ) } - TimestampPosition.Hidden -> Box(modifier) { content {} } } } @@ -775,17 +772,11 @@ private fun MessageEventBubbleContent( } } - val timestampPosition = when (val content = event.content) { - is TimelineItemImageContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay - is TimelineItemVideoContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay - is TimelineItemStickerContent -> TimestampPosition.Overlay - is TimelineItemLocationContent -> { - val content = content.ensureActiveLiveLocation() - val shouldHide = content.mode is TimelineItemLocationContent.Mode.Live && - content.mode.isActive && - content.mode.isOwnUser - if (shouldHide) TimestampPosition.Hidden else TimestampPosition.Overlay - } + val timestampPosition = when (event.content) { + is TimelineItemImageContent -> if (event.content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay + is TimelineItemVideoContent -> if (event.content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay + is TimelineItemStickerContent, + is TimelineItemLocationContent -> TimestampPosition.Overlay is TimelineItemPollContent -> TimestampPosition.Below else -> TimestampPosition.Default } @@ -864,7 +855,7 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview { ), senderId = UserId("@user:id"), senderProfile = ProfileDetails.Ready( - displayName = USER_NAME_ALICE, + displayName = "Alice", avatarUrl = null, displayNameAmbiguous = false, ), @@ -899,7 +890,7 @@ internal fun ThreadSummaryViewPreview() { ), senderId = UserId("@user:id"), senderProfile = ProfileDetails.Ready( - displayName = USER_NAME_ALICE, + displayName = "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 abacc45ef8..49328ac025 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,8 +16,6 @@ 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 @@ -27,7 +25,7 @@ internal fun TimelineItemEventRowUtdPreview() = ElementPreview { Column { ATimelineItemEventRow( event = aTimelineItemEvent( - senderDisplayName = USER_NAME_ALICE, + senderDisplayName = "Alice", isMine = false, content = TimelineItemEncryptedContent( data = UnableToDecryptContent.Data.MegolmV1AesSha2( @@ -41,7 +39,7 @@ internal fun TimelineItemEventRowUtdPreview() = ElementPreview { ) ATimelineItemEventRow( event = aTimelineItemEvent( - senderDisplayName = USER_NAME_BOB, + senderDisplayName = "Bob", isMine = false, content = TimelineItemEncryptedContent( data = UnableToDecryptContent.Data.MegolmV1AesSha2( @@ -56,7 +54,7 @@ internal fun TimelineItemEventRowUtdPreview() = ElementPreview { ATimelineItemEventRow( event = aTimelineItemEvent( - senderDisplayName = USER_NAME_BOB, + senderDisplayName = "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/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index df2b9d8691..505d76b24b 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.a11y.isTalkbackActive +import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.wysiwyg.link.Link @Composable @@ -188,6 +188,7 @@ private fun TimelineItemGroupedEventsRowContent( onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, onSwipeToReply = {}, + onJoinCallClick = {}, eventSink = eventSink, eventContentView = eventContentView, ) 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 a02413d534..469afe494e 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.a11y.isTalkbackActive +import io.element.android.libraries.ui.utils.time.isTalkbackActive import io.element.android.wysiwyg.link.Link import kotlin.time.DurationUnit @@ -72,6 +72,7 @@ internal fun TimelineItemRow( onMoreReactionsClick: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit, + onJoinCallClick: (isAudioCall: Boolean) -> Unit, eventSink: (TimelineEvent.TimelineItemEvent) -> Unit, modifier: Modifier = Modifier, eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit = @@ -125,10 +126,10 @@ 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, + roomCallState = timelineRoomInfo.roomCallState, onLongClick = onLongClick, + onJoinCallClick = onJoinCallClick, ) } else -> { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimestampPosition.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimestampPosition.kt index 505edeef15..605db65da3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimestampPosition.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimestampPosition.kt @@ -22,12 +22,7 @@ enum class TimestampPosition { /** * Timestamp should always be rendered below the timeline event content (eg. poll). */ - Below, - - /** - * Timestamp should be hidden. - */ - Hidden; + Below; companion object { /** diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt index 4ac83c520b..f42a26cf65 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt @@ -50,8 +50,7 @@ fun CustomReactionBottomSheet( ModalBottomSheet( onDismissRequest = ::onDismiss, sheetState = sheetState, - modifier = modifier, - scrollable = false, + modifier = modifier ) { val presenter = remember { EmojiPickerPresenter( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt index 2044796889..b324de8ea2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt @@ -29,8 +29,9 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPaymentContentWrapper 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.features.wallet.impl.timeline.TimelineItemPaymentView import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.voiceplayer.api.VoiceMessageState import io.element.android.wysiwyg.link.Link @@ -72,13 +73,10 @@ fun TimelineItemEventContentView( onContentLayoutChange = onContentLayoutChange, modifier = modifier ) - is TimelineItemLocationContent -> { - TimelineItemLocationView( - content = content.ensureActiveLiveLocation(), - onStopLiveLocationClick = { eventSink(TimelineEvent.StopLiveLocationShare) }, - modifier = modifier - ) - } + is TimelineItemLocationContent -> TimelineItemLocationView( + content = content, + modifier = modifier + ) is TimelineItemImageContent -> TimelineItemImageView( content = content, hideMediaContent = hideMediaContent, @@ -138,6 +136,10 @@ fun TimelineItemEventContentView( modifier = modifier ) } + is TimelineItemPaymentContentWrapper -> TimelineItemPaymentView( + content = content.paymentContent, + modifier = modifier + ) is TimelineItemRtcNotificationContent -> error("This shouldn't be rendered as the content of a bubble") } } 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 0e0a98a96c..a8cbb89e96 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,11 +54,10 @@ 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.a11y.isTalkbackActive +import io.element.android.libraries.ui.utils.time.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, @@ -80,7 +79,7 @@ fun TimelineItemImageView( Modifier } TimelineItemAspectRatioBox( - modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f).align(Alignment.CenterHorizontally), + modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f), aspectRatio = coerceRatioWhenHidingContent(content.aspectRatio, hideMediaContent), ) { ProtectedView( @@ -124,14 +123,7 @@ fun TimelineItemImageView( LocalContentColor provides ElementTheme.colors.textPrimary, LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular ) { - 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 - } + val aspectRatio = content.aspectRatio ?: DEFAULT_ASPECT_RATIO EditorStyledText( modifier = Modifier .padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout @@ -208,38 +200,3 @@ 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 4ab4ee84a6..592b95a337 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 @@ -8,154 +8,33 @@ package io.element.android.features.messages.impl.timeline.components.event -import androidx.compose.foundation.background -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.fillMaxWidth import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.size -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 -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource 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.StaticMapView import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContentProvider 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.Text -import io.element.android.libraries.ui.strings.CommonStrings @Composable fun TimelineItemLocationView( content: TimelineItemLocationContent, - onStopLiveLocationClick: () -> Unit, modifier: Modifier = Modifier, ) { - Box(modifier = modifier.fillMaxWidth()) { - StaticMapView( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 188.dp), - pinVariant = content.pinVariant, - location = content.location, - zoom = 15.0, - contentDescription = content.description - ) - - if (content.mode is TimelineItemLocationContent.Mode.Live) { - LiveLocationOverlay( - mode = content.mode, - onStopClick = onStopLiveLocationClick, - modifier = Modifier.align(Alignment.BottomStart) - ) - } - } -} - -@Composable -private fun LiveLocationOverlay( - mode: TimelineItemLocationContent.Mode.Live, - onStopClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( + StaticMapView( modifier = modifier .fillMaxWidth() - .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, - color = if (mode.isActive) ElementTheme.colors.iconQuaternaryAlpha else Color.Transparent, - shape = iconShape, - ) - .background( - color = if (mode.isActive) { - ElementTheme.colors.bgCanvasDefault - } else { - ElementTheme.colors.bgSubtleSecondary - }, - shape = iconShape - ) - ) { - if (mode.isLoading) { - CircularProgressIndicator( - strokeWidth = 2.dp, - color = ElementTheme.colors.iconSecondary, - modifier = Modifier - .align(Alignment.Center) - .size(20.dp) - ) - } else { - Icon( - imageVector = CompoundIcons.LocationPinSolid(), - contentDescription = null, - tint = if (mode.isActive) { - ElementTheme.colors.iconAccentPrimary - } else { - ElementTheme.colors.iconDisabled - }, - modifier = Modifier.align(Alignment.Center) - ) - } - } - Column(modifier = Modifier.weight(1f)) { - Text( - text = if (mode.isActive) { - stringResource(CommonStrings.common_live_location) - } else { - stringResource(CommonStrings.common_live_location_ended) - }, - style = ElementTheme.typography.fontBodySmMedium, - color = ElementTheme.colors.textPrimary, - ) - if (mode.isActive) { - Text( - text = mode.endsAt, - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textPrimary, - ) - } - } - - 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(), - contentDescription = null, - ) - } - } - } + .heightIn(max = 188.dp), + pinVariant = content.pinVariant, + lat = content.location.lat, + lon = content.location.lon, + zoom = 15.0, + contentDescription = content.body + ) } @PreviewsDayNight @@ -164,6 +43,5 @@ internal fun TimelineItemLocationViewPreview(@PreviewParameter(TimelineItemLocat ElementPreview { TimelineItemLocationView( content = content, - onStopLiveLocationClick = {}, ) } 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 8d1ef18f39..f5e760736e 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.a11y.isTalkbackActive +import io.element.android.libraries.ui.utils.time.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 86e3f1c849..5dbd0c478f 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.a11y.isTalkbackActive +import io.element.android.libraries.ui.utils.time.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/reactionsummary/ReactionSummaryView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt index 2a902fd692..03ce564eff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt @@ -90,8 +90,7 @@ fun ReactionSummaryView( if (state.target != null) { ModalBottomSheet( onDismissRequest = ::onDismiss, - modifier = modifier, - scrollable = false, + modifier = modifier ) { ReactionSummaryViewContent(summary = state.target) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt index 9637298d58..b65e326045 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt @@ -57,8 +57,7 @@ internal fun ReadReceiptBottomSheet( sheetState.hide() state.eventSink(ReadReceiptBottomSheetEvent.Dismiss) } - }, - scrollable = false, + } ) { ReadReceiptBottomSheetContent( state = state, 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 d59526d8bb..f812b40e61 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,7 +24,6 @@ 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 @@ -87,13 +86,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 0d51d9f5b8..7c36521fc0 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,9 +8,7 @@ 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 @@ -20,12 +18,6 @@ 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/event/TimelineItemContentFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt index 3b3e5118d6..3e3d77b537 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,14 +10,11 @@ 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 import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent -import io.element.android.libraries.dateformatter.api.DateFormatter -import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId @@ -38,9 +35,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName -import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.services.toolbox.api.strings.StringProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPaymentContentWrapper +import io.element.android.features.wallet.impl.timeline.TimelineItemContentPaymentFactory @Inject class TimelineItemContentFactory( @@ -54,11 +52,25 @@ class TimelineItemContentFactory( private val stateFactory: TimelineItemContentStateFactory, private val failedToParseMessageFactory: TimelineItemContentFailedToParseMessageFactory, private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory, + private val paymentFactory: TimelineItemContentPaymentFactory, private val sessionId: SessionId, - private val dateFormatter: DateFormatter, - private val stringProvider: StringProvider, ) { suspend fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent { + val isOutgoing = sessionId == eventTimelineItem.sender + + // Check for custom event types that we handle specially + val content = eventTimelineItem.content + if (content is CustomEventContent && paymentFactory.isPaymentEventType(content.eventType)) { + // Try to get raw JSON from debug info for payment events + val rawJson = eventTimelineItem.timelineItemDebugInfoProvider().originalJson + if (rawJson != null) { + val paymentContent = paymentFactory.createFromRaw(rawJson, isOutgoing) + if (paymentContent != null) { + return TimelineItemPaymentContentWrapper(paymentContent) + } + } + } + return create( itemContent = eventTimelineItem.content, eventId = eventTimelineItem.eventId, @@ -104,38 +116,29 @@ class TimelineItemContentFactory( is StickerContent -> stickerFactory.create(itemContent) is PollContent -> pollFactory.create(eventId, isEditable, isOutgoing, itemContent) is UnableToDecryptContent -> utdFactory.create(itemContent) - is CallNotifyContent -> TimelineItemRtcNotificationContent( - callIntent = itemContent.callIntent, - state = if (itemContent.declinedBy.isEmpty()) { - RtcNotificationState.Started - } else { - RtcNotificationState.Declined(itemContent.declinedBy.any { it == sessionId }) - } - ) + is CallNotifyContent -> TimelineItemRtcNotificationContent() is UnknownContent -> TimelineItemUnknownContent + is CustomEventContent -> { + // Custom events that weren't handled above (e.g., unknown custom event types) + TimelineItemUnknownContent + } is LiveLocationContent -> { val lastKnownLocation = itemContent.locations.mapNotNull { beacon -> Location.fromGeoUri(beacon.geoUri) }.lastOrNull() - - val endsAt = dateFormatter.format( - timestamp = itemContent.endTimestamp, - mode = DateFormatterMode.TimeOnly - ) - // Always create content, location can be null for "loading/waiting" state - TimelineItemLocationContent( - description = itemContent.description?.trimEnd(), - assetType = itemContent.assetType, - senderId = sender, - senderProfile = senderProfile, - mode = TimelineItemLocationContent.Mode.Live( - lastKnownLocation = lastKnownLocation, - isActive = itemContent.isLive, - endsAt = stringProvider.getString(CommonStrings.common_ends_at, endsAt), - endTimestamp = itemContent.endTimestamp, - isOwnUser = sessionId == sender - ), - ) + if (lastKnownLocation != null) { + TimelineItemLocationContent( + body = itemContent.body.trimEnd(), + description = itemContent.description?.trimEnd(), + assetType = itemContent.assetType, + senderId = sender, + senderProfile = senderProfile, + location = lastKnownLocation, + mode = TimelineItemLocationContent.Mode.Live(isActive = itemContent.isLive) + ) + } else { + TimelineItemUnknownContent + } } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index e2e5d0c03e..385956dcd1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.utils.TextPillificationHelper + import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.androidutils.text.safeLinkify import io.element.android.libraries.core.mimetype.MimeTypes @@ -150,11 +151,13 @@ class TimelineItemContentMessageFactory( ) } else { TimelineItemLocationContent( + body = body, + location = location, description = messageType.description, senderId = senderId, senderProfile = senderProfile, assetType = messageType.assetType, - mode = TimelineItemLocationContent.Mode.Static(location = location) + mode = TimelineItemLocationContent.Mode.Static ) } } @@ -252,16 +255,7 @@ class TimelineItemContentMessageFactory( } is TextMessageType -> { val body = messageType.body.trimEnd() - val dom = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser) - val formattedBody = dom?.let(::parseHtml) - ?: textPillificationHelper.pillify(body).safeLinkify() - val htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser) - TimelineItemTextContent( - body = body, - htmlDocument = htmlDocument, - formattedBody = formattedBody, - isEdited = content.isEdited, - ) + createTextContent(body, messageType, content.isEdited) } is OtherMessageType -> { val body = messageType.body.trimEnd() @@ -275,6 +269,23 @@ class TimelineItemContentMessageFactory( } } + private fun createTextContent( + body: String, + messageType: TextMessageType, + isEdited: Boolean, + ): TimelineItemTextContent { + val dom = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser) + val formattedBody = dom?.let(::parseHtml) + ?: textPillificationHelper.pillify(body).safeLinkify() + val htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser) + return TimelineItemTextContent( + body = body, + htmlDocument = htmlDocument, + formattedBody = formattedBody, + isEdited = isEdited, + ) + } + private fun aspectRatioOf(width: Long?, height: Long?): Float? { val result = if (height != null && width != null) { width.toFloat() / height.toFloat() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt index 837692ae6f..e2e46a86af 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt @@ -26,8 +26,10 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPaymentContentWrapper import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent @@ -63,6 +65,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean { TimelineItemUnknownContent, is TimelineItemLegacyCallInviteContent, is TimelineItemRtcNotificationContent -> false + is TimelineItemPaymentContentWrapper -> false is TimelineItemProfileChangeContent, is TimelineItemRoomMembershipContent, is TimelineItemStateEventContent -> true @@ -90,7 +93,8 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean { is RoomMembershipContent, UnknownContent, is LegacyCallInviteContent, - is CallNotifyContent, - is StateContent -> false + CallNotifyContent, + is StateContent, + is CustomEventContent -> false } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt index 9c4c48d11e..14902e1f82 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt @@ -83,7 +83,8 @@ fun TimelineItemEventContent.canReact(): Boolean = is TimelineItemRedactedContent, is TimelineItemLegacyCallInviteContent, is TimelineItemRtcNotificationContent, - TimelineItemUnknownContent -> false + TimelineItemUnknownContent, + is TimelineItemPaymentContentWrapper -> false } /** 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 52e008e121..f3d70f44e7 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 mode.lastKnownLocation - is Mode.Static -> mode.location - } - - /** - * The pin variant to display on the map. - * Returns a default variant when location is null (map will show loading placeholder anyway). - */ - val pinVariant: PinVariant = when (mode) { + val pinVariant = when (mode) { is Mode.Live -> { if (mode.isActive) { PinVariant.UserLocation(avatarData = senderAvatar(), isLive = true) @@ -45,7 +35,7 @@ data class TimelineItemLocationContent( PinVariant.StaleLocation } } - is Mode.Static -> { + Mode.Static -> { when (assetType) { AssetType.PIN -> PinVariant.PinnedLocation AssetType.SENDER, @@ -63,58 +53,9 @@ data class TimelineItemLocationContent( ) sealed interface Mode { - data class Static( - val location: Location, - ) : Mode - - data class Live( - val lastKnownLocation: Location?, - val isActive: Boolean, - val endsAt: String, - val endTimestamp: Long, - val isOwnUser: Boolean, - ) : Mode { - val isLoading = lastKnownLocation == null && isActive - val canStopSharing = isActive && isOwnUser - } + data object Static : Mode + data class Live(val isActive: Boolean) : Mode } override val type: String = "TimelineItemLocationContent" } - -/** - * Overrides the isActive value if needed, to make sure endTimestamp is used in absence of stop event. - */ -@Composable -internal fun TimelineItemLocationContent.ensureActiveLiveLocation( - currentTimeMillis: () -> Long = System::currentTimeMillis, -): TimelineItemLocationContent { - return when (mode) { - is TimelineItemLocationContent.Mode.Live -> { - val isActive = rememberIsLiveLocationActive(mode, currentTimeMillis) - copy(mode = mode.copy(isActive = isActive)) - } - is TimelineItemLocationContent.Mode.Static -> this - } -} - -@Composable -private fun rememberIsLiveLocationActive( - mode: TimelineItemLocationContent.Mode.Live, - currentTimeMillis: () -> Long, -): Boolean { - fun TimelineItemLocationContent.Mode.Live.isActive(): Boolean { - return isActive && endTimestamp > currentTimeMillis() - } - return produceState( - initialValue = mode.isActive(), - key1 = mode.endTimestamp, - key2 = mode.isActive, - ) { - if (mode.isActive) { - val remainingMillis = mode.endTimestamp - currentTimeMillis() - delay(remainingMillis) - } - value = false - }.value -} 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 6c095c828e..362e9b4cda 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,53 +17,25 @@ import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsRead open class TimelineItemLocationContentProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aTimelineItemLocationContent( - mode = aStaticLocationMode() - ), - aTimelineItemLocationContent( - mode = aLiveLocationMode(isActive = true) - ), - aTimelineItemLocationContent( - mode = aLiveLocationMode(isActive = true, lastKnownLocation = null) - ), - aTimelineItemLocationContent( - mode = aLiveLocationMode(isActive = true, isOwnUser = false) - ), - aTimelineItemLocationContent( - mode = aLiveLocationMode(isActive = false) - ), + aTimelineItemLocationContent(), + aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(isActive = true)), + aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(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( + body: String = "", senderId: UserId = UserId("@sender:matrix.org"), senderProfile: ProfileDetails = aProfileDetailsReady(), - description: String? = null, - mode: TimelineItemLocationContent.Mode, + mode: TimelineItemLocationContent.Mode = TimelineItemLocationContent.Mode.Static, ) = TimelineItemLocationContent( + body = body, + location = Location( + lat = 52.2445, + lon = 0.7186, + accuracy = 5000f, + ), senderId = senderId, senderProfile = senderProfile, - description = description, - mode = mode, -) - -fun aLocation() = Location( - lat = 52.2445, - lon = 0.7186, - accuracy = 5000f, + mode = mode ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPaymentContentWrapper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPaymentContentWrapper.kt new file mode 100644 index 0000000000..d0af0290e3 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPaymentContentWrapper.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.messages.impl.timeline.model.event + +import androidx.compose.runtime.Immutable +import io.element.android.features.wallet.api.PaymentCardStatus +import io.element.android.features.wallet.api.timeline.TimelineItemPaymentContent + +/** + * Wrapper for [TimelineItemPaymentContent] that implements [TimelineItemEventContent]. + * + * This wrapper is necessary because [TimelineItemEventContent] is a sealed interface + * that must have all implementers in the same module. Since the wallet module + * cannot add types to the sealed hierarchy, we wrap the payment content here. + */ +@Immutable +data class TimelineItemPaymentContentWrapper( + val paymentContent: TimelineItemPaymentContent, +) : TimelineItemEventContent { + override val type: String = paymentContent.type + + // Delegate properties for convenience + val amountLovelace: Long get() = paymentContent.amountLovelace + val toAddress: String get() = paymentContent.toAddress + val fromAddress: String get() = paymentContent.fromAddress + val txHash: String? get() = paymentContent.txHash + val status: PaymentCardStatus get() = paymentContent.status + val network: String get() = paymentContent.network + val isSentByMe: Boolean get() = paymentContent.isSentByMe + val fallbackText: String get() = paymentContent.fallbackText + val amountAda: String get() = paymentContent.amountAda + val isTestnet: Boolean get() = paymentContent.isTestnet + val truncatedTxHash: String? get() = paymentContent.truncatedTxHash + val truncatedToAddress: String get() = paymentContent.truncatedToAddress + val truncatedFromAddress: String get() = paymentContent.truncatedFromAddress + val explorerUrl: String? get() = paymentContent.explorerUrl +} 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 2359f196a9..00ad32ba5f 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 @@ -8,20 +8,6 @@ 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 - -// 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 +class TimelineItemRtcNotificationContent : TimelineItemEventContent { + override val type: String = "org.matrix.msc4075.rtc.notification" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt index 2dbe47e9e4..de55735b76 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.semantics.Role 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.theme.Theme import io.element.android.features.messages.impl.timeline.components.event.TimelineItemAspectRatioBox import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground import io.element.android.libraries.designsystem.preview.ElementPreview @@ -50,7 +49,7 @@ fun ProtectedView( .background(Color(0x99000000)), contentAlignment = Alignment.Center, ) { - ElementTheme(theme = Theme.Light, applySystemBarsUpdate = false) { + ElementTheme(darkTheme = false, applySystemBarsUpdate = false) { // Not using a button to be able to have correct size Text( modifier = Modifier 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..2f3602dcd1 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 @@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPaymentContentWrapper /** * Return true if the event must be hidden by default when the setting to hide images and videos is enabled. @@ -53,7 +54,8 @@ fun TimelineItem.mustBeProtected(): Boolean { is TimelineItemNoticeContent, is TimelineItemTextContent, TimelineItemUnknownContent, - is TimelineItemVoiceContent -> false + is TimelineItemVoiceContent, + is TimelineItemPaymentContentWrapper -> false } is TimelineItem.Virtual -> false is TimelineItem.GroupedEvents -> false 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 4f023ad2bd..f54f93123d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt @@ -15,7 +15,6 @@ 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 @@ -43,7 +42,6 @@ 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 @@ -80,8 +78,7 @@ internal fun MessagesViewTopBar( Row( modifier = Modifier .clip(roundedCornerShape) - .clickable { onRoomDetailsClick() } - .semantics { heading() }, + .clickable { onRoomDetailsClick() }, horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -94,12 +91,9 @@ 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, @@ -107,7 +101,6 @@ internal fun MessagesViewTopBar( } IdentityState.VerificationViolation -> { Icon( - modifier = iconModifier, imageVector = CompoundIcons.ErrorSolid(), tint = ElementTheme.colors.iconCriticalPrimary, contentDescription = null, @@ -119,13 +112,11 @@ 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), @@ -159,7 +150,10 @@ private fun RoomAvatarAndNameRow( ) Text( modifier = Modifier - .padding(start = 8.dp), + .padding(horizontal = 8.dp) + .semantics { + heading() + }, text = roomName ?: stringResource(CommonStrings.common_no_room_name), style = ElementTheme.typography.fontBodyLgMedium, fontStyle = FontStyle.Italic.takeIf { roomName == null }, @@ -174,9 +168,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, @@ -184,6 +178,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview { roomCallState: RoomCallState = RoomCallState.Unavailable, dmUserIdentityState: IdentityState? = null, sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE, + isDmRoom: Boolean = false, displayThreads: Boolean = false, ) = MessagesViewTopBar( roomName = roomName, @@ -200,6 +195,8 @@ internal fun MessagesViewTopBarPreview() = ElementPreview { displayThreads = displayThreads, onJoinCallClick = {}, onThreadsListClick = {}, + isDmRoom = isDmRoom, + onWalletClick = {}, ) } ) @@ -223,7 +220,8 @@ internal fun MessagesViewTopBarPreview() = ElementPreview { url = "https://some-avatar.jpg" ), roomCallState = aStandByCallState(canStartCall = false), - dmUserIdentityState = IdentityState.Verified + dmUserIdentityState = IdentityState.Verified, + isDmRoom = true, ) HorizontalDivider() AMessagesViewTopBar( 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 5ef4541f06..2247566531 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,9 +17,8 @@ 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 @@ -32,7 +31,6 @@ 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 @@ -59,18 +57,7 @@ internal fun ThreadTopBar( BackButton(onClick = onBackClick) }, title = { - 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 - }, - ) { + Row(verticalAlignment = Alignment.CenterVertically) { Avatar( avatarData = roomAvatarData, avatarType = AvatarType.Room( @@ -81,14 +68,17 @@ internal fun ThreadTopBar( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp), + .padding(horizontal = 8.dp) + .semantics { + heading() + }, ) { Text( text = stringResource(CommonStrings.common_thread), style = ElementTheme.typography.fontBodyLgMedium, ) Text( - text = name, + text = roomName ?: stringResource(CommonStrings.common_no_room_name), style = ElementTheme.typography.fontBodySmRegular, fontStyle = FontStyle.Italic.takeIf { roomName == null }, color = ElementTheme.colors.textSecondary, @@ -106,9 +96,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, @@ -133,7 +123,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 e298b3af26..0506026b86 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,11 +9,6 @@ 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 { @@ -27,7 +22,7 @@ class TypingNotificationStateProvider : PreviewParameterProvider content.plainText is TimelineItemProfileChangeContent -> content.body is TimelineItemStateContent -> content.body - is TimelineItemLocationContent -> when (content.mode) { - is TimelineItemLocationContent.Mode.Live -> context.getString(CommonStrings.common_shared_live_location) - is TimelineItemLocationContent.Mode.Static -> context.getString(CommonStrings.common_shared_location) - } + is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location) is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt) is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed) is TimelineItemPollContent -> content.question @@ -57,16 +54,8 @@ class DefaultMessageSummaryFormatter( is TimelineItemFileContent -> context.getString(CommonStrings.common_file) is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio) is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_unsupported_call) - 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) - } + is TimelineItemRtcNotificationContent -> context.getString(CommonStrings.common_call_started) + is TimelineItemPaymentContentWrapper -> "Payment" } // Truncate the message to a safe length to avoid crashes in Compose .toSafeLength() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt index 96d1787085..6fdd2f1752 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt @@ -34,12 +34,10 @@ import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.audio.api.AudioFocusRequester import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.annotations.SessionCoroutineScope -import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaupload.api.MediaSenderFactory import io.element.android.libraries.permissions.api.PermissionsEvent import io.element.android.libraries.permissions.api.PermissionsPresenter -import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState @@ -153,7 +151,7 @@ class DefaultVoiceMessageComposerPresenter( } } - fun sendVoiceMessage(inReplyToEventId: EventId?) { + fun sendVoiceMessage() { val finishedState = recorderState as? VoiceRecorderState.Finished if (finishedState == null) { val exception = VoiceMessageException.FileException("No file to send") @@ -172,7 +170,6 @@ class DefaultVoiceMessageComposerPresenter( file = finishedState.file, mimeType = finishedState.mimeType, waveform = finishedState.waveform, - inReplyToEventId = inReplyToEventId, ) if (result.isFailure) { showSendFailureDialog = true @@ -186,13 +183,8 @@ class DefaultVoiceMessageComposerPresenter( when (event) { is VoiceMessageComposerEvent.RecorderEvent -> handleVoiceMessageRecorderEvent(event.recorderEvent) is VoiceMessageComposerEvent.PlayerEvent -> handleVoiceMessagePlayerEvent(event.playerEvent) - is VoiceMessageComposerEvent.SendVoiceMessage -> { - // Capture reply info eagerly before any coroutine dispatch, since CloseSpecialMode - // may reset composerMode before the coroutine runs. - val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId - localCoroutineScope.launch { - sendVoiceMessage(inReplyToEventId) - } + is VoiceMessageComposerEvent.SendVoiceMessage -> localCoroutineScope.launch { + sendVoiceMessage() } VoiceMessageComposerEvent.DeleteVoiceMessage -> { player.pause() @@ -288,13 +280,11 @@ class DefaultVoiceMessageComposerPresenter( file: File, mimeType: String, waveform: List, - inReplyToEventId: EventId? = null, ): Result { val result = mediaSender.sendVoiceMessage( uri = file.toUri(), mimeType = mimeType, waveForm = waveform, - inReplyToEventId = inReplyToEventId, ) if (result.isFailure) { diff --git a/features/messages/impl/src/main/res/values-ca/translations.xml b/features/messages/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index f9b2685c7d..0000000000 --- a/features/messages/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - "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 9bf1c5c079..93ea0d8178 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." - "Pravost této šifrované zprávy nelze na tomto zařízení zaručit." + "Autenticitu této zaš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 e068df293a..390d03f451 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 placering" + "Del lokation" "Afstemning" "Tekstformatering" "Beskedhistorikken er i øjeblikket ikke tilgængelig." diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml index 4848f1f282..5323e731e9 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -35,7 +35,7 @@ "Video aufnehmen" "Anhang" "Foto- und Videogalerie" - "Standort teilen" + "Standort" "Umfrage" "Textformatierung" "Der Nachrichtenverlauf ist derzeit nicht verfügbar" 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 506b5ce195..8da4d23f79 100644 --- a/features/messages/impl/src/main/res/values-et/translations.xml +++ b/features/messages/impl/src/main/res/values-et/translations.xml @@ -35,7 +35,7 @@ "Salvesta video" "Manus" "Fotode ja videote galerii" - "Jaga asukohta" + "Asukoht" "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 19f55a5559..446239c095 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,13 +56,5 @@ "این اتاق جایگزین شده و دیگر فعّال نیست" "دیدن پیام‌های قدیمی" "این اتاق ادامهٔ اتاقی دیگر است" - - "%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-hr/translations.xml b/features/messages/impl/src/main/res/values-hr/translations.xml index 03aeae7a0e..3da55fc4d7 100644 --- a/features/messages/impl/src/main/res/values-hr/translations.xml +++ b/features/messages/impl/src/main/res/values-hr/translations.xml @@ -35,7 +35,7 @@ "Snimi videozapis" "Privitak" "Biblioteka fotografija i videozapisa" - "Dijeli lokaciju" + "Lokacija" "Anketa" "Oblikovanje teksta" "Povijest poruka trenutačno nije dostupna." 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 7a0994f65e..508f4d5476 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" - "Membagi Lokasi" + "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 93c10d13d8..4fd18e8db5 100644 --- a/features/messages/impl/src/main/res/values-ja/translations.xml +++ b/features/messages/impl/src/main/res/values-ja/translations.xml @@ -26,7 +26,6 @@ "個数 %1$d / %2$d" "画像の品質を最適化" "処理中…" - "メディアを追加" "ユーザーをブロック" "このユーザーからのメッセージをすべて非表示にする場合はチェックしてください。" "このメッセージはホームサーバーの管理者に報告されます。暗号化されたメッセージを確認することはできません。" @@ -35,7 +34,7 @@ "写真を撮影" "動画を撮影" "添付ファイル" - "アルバムの写真と動画" + "アルバムの写真・動画" "場所を共有" "投票" "書式設定" 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 a0b9c24af2..18a8af6cb0 100644 --- a/features/messages/impl/src/main/res/values-pl/translations.xml +++ b/features/messages/impl/src/main/res/values-pl/translations.xml @@ -26,17 +26,16 @@ "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 wideo" + "Nagraj film" "Załącznik" "Zdjęcia i filmy" - "Udostępnij lokalizację" + "Lokalizacja" "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 2fb5173169..054d2a1759 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" - "Partilhar localização" + "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-ro/translations.xml b/features/messages/impl/src/main/res/values-ro/translations.xml index 67b72dc2a8..ff380874ea 100644 --- a/features/messages/impl/src/main/res/values-ro/translations.xml +++ b/features/messages/impl/src/main/res/values-ro/translations.xml @@ -35,7 +35,7 @@ "Înregistrați un videoclip" "Atașament" "Bibliotecă foto și video" - "Partajați locația" + "Locație" "Sondaj" "Formatarea textului" "Mesajele anterioare nu sunt momentan disponibile în această cameră" 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 d2022c4882..4f267d8552 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í" - "Zdieľať polohu" + "Poloha" "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 a3035e9e08..c51744a408 100644 --- a/features/messages/impl/src/main/res/values-uk/translations.xml +++ b/features/messages/impl/src/main/res/values-uk/translations.xml @@ -35,7 +35,7 @@ "Записати відео" "Вкладення" "Бібліотека фото та відео" - "Поділитися місцеперебуванням" + "Розташування" "Опитування" "Форматування тексту" "Історія повідомлень наразі недоступна." diff --git a/features/messages/impl/src/main/res/values-vi/translations.xml b/features/messages/impl/src/main/res/values-vi/translations.xml index 5104cd5a5b..81ac0b7b8e 100644 --- a/features/messages/impl/src/main/res/values-vi/translations.xml +++ b/features/messages/impl/src/main/res/values-vi/translations.xml @@ -14,7 +14,6 @@ "Đồ vật" "Mặt cười & mọi người" "Du lịch và địa danh" - "Biểu tượng cảm xúc gần đây" "Biểu tượng" "Xử lý phương tiện tải lên không thành công, vui lòng thử lại." "Không thể tải lên tệp phương tiện. Vui lòng thử lại." @@ -45,12 +44,6 @@ "Thu gọn" "Đã sao chép tin nhắn" "Bạn không có quyền gửi tin nhắn trong phòng này" - - "%1$d thành viên đã phản ứng với %2$s" - - - "Bạn và thành viên %1$d đã phản ứng với%2$s" - "Thu gọn" "Xem thêm" "Mới" diff --git a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml index 0eea6e808e..321c381359 100644 --- a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml @@ -35,7 +35,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 bdd898d26f..2a6b9bf78d 100644 --- a/features/messages/impl/src/main/res/values-zh/translations.xml +++ b/features/messages/impl/src/main/res/values-zh/translations.xml @@ -7,13 +7,13 @@ "由未知或已删除的设备加密。" "由未经其所有者验证的设备加密。" "由未经验证的用户加密。" - "节假日" + "活动" "旗帜" - "饮食" + "食物和饮料" "动物和自然" - "日常物品" + "物品" "表情和人物" - "文旅景点" + "旅行和地点" "最近的 Emoji" "符号" "使用旧版应用程序的用户可能无法看到字幕。" @@ -21,16 +21,15 @@ "无法上传该文件。" "处理要上传的媒体失败,请重试。" "上传媒体失败,请重试。" - "允许的最大文件大小为 %1$s。" + "允许的最大文件大小为%1$s 。" "文件太大,无法上传" - "第 %1$d 个项目,共 %2$d 个" + "第%1$d/%2$d项" "优化图像质量" "处理中…" - "添加媒体" "屏蔽用户" - "请确认是否要隐藏该用户当前和未来的所有消息" - "此消息将举报给服务器管理员。他们无法读取任何加密消息。" - "举报此内容的理由" + "请确认是否要隐藏该用户当前和未来的所有信息" + "此消息将举报给您的服务器管理员。他们无法读取任何加密消息。" + "举报此内容的原因" "相机" "拍摄照片" "录制视频" @@ -40,40 +39,40 @@ "投票" "文本格式化" "消息历史记录当前不可用。" - "消息历史在此房间不可用。请验证此设备以查看。" - "你想邀请他们回来吗?" - "此聊天中只有你一人" - "通知整个房间" + "此聊天室无法查看消息历史记录。请验证此设备以查看之。" + "您想邀请他们回来吗?" + "此聊天室中只有您一个人" + "通知整个聊天室" "所有人" "再次发送" "消息发送失败" - "添加反应" - "这是房间 %1$s 的开头。" + "添加表情符号" + "这是 %1$s 聊天室的开始。" "这是本对话的开始。" - "不受支持的通话。询问呼叫方是否可以使用新的 Element X app。" + "不支持的呼叫。询问呼叫者是否可以使用新的 Element X 应用程序。" "折叠" "消息已复制" - "你无权在此房间发言" + "您无权在此聊天室发言" - "%1$d 个成员使用 %2$s 反应" + "%1$d 个成员添加表情符号 %2$s" - "你与其他 %1$d 个成员使用 %2$s 反应" + "您与 %1$d 个成员添加表情符号 %2$s" - "你使用 %1$s 反应" + "您添加了表情符号%1$s" "折叠" "展开" "显示反应摘要" "新消息" - "%1$d 个房间变化" + "%1$d 个聊天室变化" - "跳转到新房间" + "跳转至新房间" "本房间已被替换,现已失效" "查看历史消息" - "此房间是另一房间的延续" + "该聊天室是其他聊天室的延续" - "%1$s,%2$s 及其他 %3$d 人" + "%1$s,%2$s 和其他 %3$d 个人" "%1$s 正在输入" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 908646b048..f5629f5e2d 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -16,12 +16,6 @@ "Travel & Places" "Recent emojis" "Symbols" - "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." @@ -32,7 +26,6 @@ "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/FakeMessagesNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt index 68f9a8d17a..44d82f1a7c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt @@ -28,7 +28,6 @@ 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) @@ -66,10 +65,6 @@ 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 65aa1d857e..6e12c607d8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -13,7 +13,6 @@ 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 @@ -121,7 +120,6 @@ 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 { @@ -142,39 +140,6 @@ 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)) } } @@ -598,7 +563,7 @@ class MessagesPresenterTest { baseRoom = FakeBaseRoom( roomPermissions = roomPermissions(), ).apply { - givenRoomInfo(aRoomInfo(isDm = true, joinedMembersCount = 1, activeMembersCount = 1)) + givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 1, activeMembersCount = 1)) }, typingNoticeResult = { Result.success(Unit) }, ) @@ -1112,7 +1077,7 @@ class MessagesPresenterTest { canRedactOwn = true, canPinUnpin = true, ), - initialRoomInfo = aRoomInfo(isDm = true, isEncrypted = true) + initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true) ).apply { givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2)))) }, @@ -1263,6 +1228,9 @@ class MessagesPresenterTest { initialRoomInfo = aRoomInfo(isEncrypted = true, historyVisibility = RoomHistoryVisibility.Shared), ), ), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.EnableKeyShareOnInvite.key to true) + ) ) presenter.testWithLifecycleOwner { awaitItem() @@ -1281,6 +1249,9 @@ class MessagesPresenterTest { initialRoomInfo = aRoomInfo(isEncrypted = true, historyVisibility = RoomHistoryVisibility.WorldReadable), ), ), + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.EnableKeyShareOnInvite.key to true) + ) ) presenter.testWithLifecycleOwner { awaitItem() @@ -1382,7 +1353,6 @@ class MessagesPresenterTest { actionListEventSink: (ActionListEvent) -> Unit = {}, addRecentEmoji: AddRecentEmoji = AddRecentEmoji { _ -> lambdaError() }, markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead(), - liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), ): MessagesPresenter { return MessagesPresenter( navigator = navigator, @@ -1412,7 +1382,6 @@ 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 be44c64a5a..62b9eac68d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -6,15 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onAllNodesWithTag @@ -27,7 +25,6 @@ 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 @@ -81,78 +78,82 @@ import io.element.android.tests.testutils.pressBack 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() + @Test - fun `clicking on back invoke expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invoke expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - setMessagesView( + rule.setMessagesView( state = state, onBackClick = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on room name invoke expected callback`() = runAndroidComposeUiTest { + fun `clicking on room name invoke expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - setMessagesView( + rule.setMessagesView( state = state, onRoomDetailsClick = callback, ) - onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick() + rule.onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick() } } @Test - fun `clicking on join call invoke expected callback`() = runAndroidComposeUiTest { + fun `clicking on join call invoke expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder ) ensureCalledOnceWithParam(false) { callback -> - setMessagesView( + rule.setMessagesView( state = state, onJoinCallClick = callback, ) - val joinCallContentDescription = activity!!.getString(CommonStrings.a11y_start_call) - onNodeWithContentDescription(joinCallContentDescription).performClick() + val joinCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_call) + rule.onNodeWithContentDescription(joinCallContentDescription).performClick() } } @Test - fun `clicking on join voice call invoke expected callback`() = runAndroidComposeUiTest { + fun `clicking on join voice call invoke expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder, roomCallState = aStandByCallState(isDM = true) ) ensureCalledOnceWithParam(true) { callback -> - setMessagesView( + rule.setMessagesView( state = state, onJoinCallClick = callback, ) - val joinVoiceCallContentDescription = activity!!.getString(CommonStrings.a11y_start_voice_call) - onNodeWithContentDescription(joinVoiceCallContentDescription).performClick() + val joinVoiceCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_voice_call) + rule.onNodeWithContentDescription(joinVoiceCallContentDescription).performClick() } } @Test - fun `clicking on an Event invoke expected callback`() = runAndroidComposeUiTest { + fun `clicking on an Event invoke expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( timelineState = aTimelineState( @@ -166,12 +167,12 @@ class MessagesViewTest { expectedParam2 = timelineItem, result = true, ) - setMessagesView( + rule.setMessagesView( state = state, onEventClick = callback, ) // Cannot perform click on "Text", it's not detected. Use tag instead - onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick() + rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick() callback.assertSuccess() } @@ -201,7 +202,7 @@ class MessagesViewTest { userHasPermissionToRedactOther: Boolean = false, userHasPermissionToSendReaction: Boolean = false, userCanPinEvent: Boolean = false, - ) = runAndroidComposeUiTest { + ) { val eventsRecorder = EventsRecorder() val state = aMessagesState( actionListState = anActionListState( @@ -219,11 +220,11 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - setMessagesView( + rule.setMessagesView( state = state, ) // Cannot perform click on "Text", it's not detected. Use tag instead - onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() } + rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() } eventsRecorder.assertSingle( ActionListEvent.ComputeForMessage( event = timelineItem, @@ -234,7 +235,7 @@ class MessagesViewTest { @Test @Config(qualifiers = "h1024dp") - fun `clicking on a read receipt list emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on a read receipt list emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -254,10 +255,10 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - setMessagesView( + rule.setMessagesView( state = state, ) - onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick() + rule.onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick() eventsRecorder.assertSingle(ReadReceiptBottomSheetEvent.EventSelected(timelineItem)) } @@ -271,7 +272,7 @@ class MessagesViewTest { swipeTest(userHasPermissionToSendMessage = false) } - private fun swipeTest(userHasPermissionToSendMessage: Boolean) = runAndroidComposeUiTest { + private fun swipeTest(userHasPermissionToSendMessage: Boolean) { val eventsRecorder = EventsRecorder() val canBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = true) val cannotBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = false) @@ -284,10 +285,10 @@ class MessagesViewTest { ), eventSink = eventsRecorder, ) - setMessagesView( + rule.setMessagesView( state = state, ) - onAllNodesWithTag(TestTags.messageBubble.value).apply { + rule.onAllNodesWithTag(TestTags.messageBubble.value).apply { onFirst().performTouchInput { swipeRight(endX = 200f) } onLast().performTouchInput { swipeRight(endX = 200f) } } @@ -299,7 +300,7 @@ class MessagesViewTest { } @Test - fun `clicking on send location invoke expected callback`() = runAndroidComposeUiTest { + fun `clicking on send location invoke expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( composerState = aMessageComposerState( @@ -308,16 +309,16 @@ class MessagesViewTest { eventSink = eventsRecorder ) ensureCalledOnce { callback -> - setMessagesView( + rule.setMessagesView( state = state, onSendLocationClick = callback, ) - clickOn(R.string.screen_room_attachment_source_location) + rule.clickOn(R.string.screen_room_attachment_source_location) } } @Test - fun `clicking on create poll invoke expected callback`() = runAndroidComposeUiTest { + fun `clicking on create poll invoke expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( composerState = aMessageComposerState( @@ -326,25 +327,25 @@ class MessagesViewTest { eventSink = eventsRecorder ) ensureCalledOnce { callback -> - setMessagesView( + rule.setMessagesView( state = state, onCreatePollClick = callback, ) // Then click on the poll action - clickOn(R.string.screen_room_attachment_source_poll) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on the avatar of the sender of an Event emits the expected event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( eventSink = eventsRecorder ) val timelineEvent = state.timelineState.timelineItems.filterIsInstance().first() - setMessagesView(state = state) - onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() + rule.setMessagesView(state = state) + rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() eventsRecorder.assertSingle( MessagesEvent.OnUserClicked( MatrixUser( @@ -358,12 +359,12 @@ class MessagesViewTest { @Test @Config(qualifiers = "h1024dp") - fun `clicking on the display name of the sender of an Event emits expected event`() = runAndroidComposeUiTest { + fun `clicking on the display name of the sender of an Event emits expected event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState(eventSink = eventsRecorder) val timelineEvent = state.timelineState.timelineItems.filterIsInstance().first() - setMessagesView(state = state) - onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() + rule.setMessagesView(state = state) + rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() eventsRecorder.assertSingle( MessagesEvent.OnUserClicked( MatrixUser( @@ -376,7 +377,7 @@ class MessagesViewTest { } @Test - fun `selecting a action on a message emits the expected Event`() = runAndroidComposeUiTest { + fun `selecting a action on a message emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( eventSink = eventsRecorder @@ -394,17 +395,17 @@ class MessagesViewTest { ) ), ) - setMessagesView( + rule.setMessagesView( state = stateWithMessageAction, ) - clickOn(CommonStrings.action_edit) + rule.clickOn(CommonStrings.action_edit) // Give time for the close animation to complete - mainClock.advanceTimeBy(milliseconds = 1_000) + rule.mainClock.advanceTimeBy(milliseconds = 1_000) eventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.Edit, timelineItem)) } @Test - fun `clicking on a reaction emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on a reaction emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -413,10 +414,10 @@ class MessagesViewTest { eventSink = eventsRecorder, ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - setMessagesView( + rule.setMessagesView( state = state, ) - onAllNodesWithText( + rule.onAllNodesWithText( text = "👍️", useUnmergedTree = true, ).onFirst().performClick() @@ -424,7 +425,7 @@ class MessagesViewTest { } @Test - fun `long clicking on a reaction emits the expected Event`() = runAndroidComposeUiTest { + fun `long clicking on a reaction emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -436,10 +437,10 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - setMessagesView( + rule.setMessagesView( state = state, ) - onAllNodesWithText( + rule.onAllNodesWithText( text = "👍️", useUnmergedTree = true, ).onFirst().performTouchInput { longClick() } @@ -447,7 +448,7 @@ class MessagesViewTest { } @Test - fun `clicking on more reaction emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on more reaction emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -458,16 +459,16 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - setMessagesView( + rule.setMessagesView( state = state, ) - val moreReactionContentDescription = activity!!.getString(R.string.screen_room_timeline_add_reaction) - onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick() + val moreReactionContentDescription = rule.activity.getString(R.string.screen_room_timeline_add_reaction) + rule.onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick() eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem)) } @Test - fun `clicking on more reaction from action list emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on more reaction from action list emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -490,18 +491,18 @@ class MessagesViewTest { eventSink = eventsRecorder ), ) - setMessagesView( + rule.setMessagesView( state = stateWithActionListState, ) - val moreReactionContentDescription = activity!!.getString(CommonStrings.a11y_react_with_other_emojis) - onNodeWithContentDescription(moreReactionContentDescription).performClick() + val moreReactionContentDescription = rule.activity.getString(CommonStrings.a11y_react_with_other_emojis) + rule.onNodeWithContentDescription(moreReactionContentDescription).performClick() // Give time for the close animation to complete - mainClock.advanceTimeBy(milliseconds = 1_000) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on verified user send failure from action list emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState() val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event @@ -518,21 +519,21 @@ class MessagesViewTest { ), timelineState = aTimelineState(eventSink = eventsRecorder) ) - setMessagesView( + rule.setMessagesView( state = stateWithActionListState, ) // Clear initial 'LoadMore' event emitted when setting the state eventsRecorder.clear() - val verifiedUserSendFailure = activity!!.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice") - onNodeWithText(verifiedUserSendFailure).performClick() + val verifiedUserSendFailure = rule.activity.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice") + rule.onNodeWithText(verifiedUserSendFailure).performClick() // Give time for the close animation to complete - mainClock.advanceTimeBy(milliseconds = 1_000) + rule.mainClock.advanceTimeBy(milliseconds = 1_000) eventsRecorder.assertSingle(TimelineEvent.ComputeVerifiedUserSendFailure(timelineItem)) } @Test - fun `clicking on a custom emoji emits the expected Events`() = runAndroidComposeUiTest { + fun `clicking on a custom emoji emits the expected Events`() { val aUnicode = "🙈" val customReactionStateEventsRecorder = EventsRecorder() val eventsRecorder = EventsRecorder() @@ -562,18 +563,18 @@ class MessagesViewTest { eventSink = customReactionStateEventsRecorder ), ) - setMessagesView( + rule.setMessagesView( state = stateWithCustomReactionState, ) - onNodeWithText(aUnicode, useUnmergedTree = true).performClick() + rule.onNodeWithText(aUnicode, useUnmergedTree = true).performClick() // Give time for the close animation to complete - mainClock.advanceTimeBy(milliseconds = 1_000) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on pinned messages banner emits the expected Event`() { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState(eventSink = eventsRecorder), @@ -586,16 +587,16 @@ class MessagesViewTest { ), ), ) - setMessagesView(state = state) + rule.setMessagesView(state = state) // Clear initial 'LoadMore' event emitted when setting the state eventsRecorder.clear() - onNodeWithText("This is a pinned message").performClick() + rule.onNodeWithText("This is a pinned message").performClick() eventsRecorder.assertSingle(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)) } @Test - fun `clicking on successor room button emits expected event`() = runAndroidComposeUiTest { + fun `clicking on successor room button emits expected event`() { val eventsRecorder = EventsRecorder() val successorRoomId = RoomId("!successor:server.org") val state = aMessagesState( @@ -605,18 +606,18 @@ class MessagesViewTest { ), timelineState = aTimelineState(eventSink = eventsRecorder) ) - setMessagesView(state = state) + rule.setMessagesView(state = state) // Clear initial 'LoadMore' event emitted when setting the state eventsRecorder.clear() - val text = activity!!.getString(R.string.screen_room_timeline_tombstoned_room_action) + val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action) // The bottomsheet subcompose seems to make the node to appear twice - onAllNodesWithText(text).onFirst().performClick() + rule.onAllNodesWithText(text).onFirst().performClick() eventsRecorder.assertSingle(TimelineEvent.NavigateToPredecessorOrSuccessorRoom(successorRoomId)) } @Test - fun `clicking on threads list button calls the expected function`() = runAndroidComposeUiTest { + fun `clicking on threads list button calls the expected function`() { val state = aMessagesState( threads = MessagesState.Threads( hasThreads = true, @@ -624,66 +625,28 @@ class MessagesViewTest { ) ) val onThreadsListClicked = lambdaRecorder {} - setMessagesView( + rule.setMessagesView( state = state, onThreadsListClicked = onThreadsListClicked, ) - onNodeWithContentDescription("Threads").performClick() + rule.onNodeWithContentDescription("Threads").performClick() onThreadsListClicked.assertions().isCalledOnce() } @Test - fun `no banner shown when there is no successor room`() = runAndroidComposeUiTest { + fun `no banner shown when there is no successor room`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( successorRoom = null, eventSink = eventsRecorder ) - 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) + rule.setMessagesView(state = state) + rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_message) + rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_action) } } -private fun AndroidComposeUiTest.setMessagesView( +private fun AndroidComposeTestRule.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 20b636081a..7a99ad39aa 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,7 +17,6 @@ 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 @@ -29,7 +28,6 @@ import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.api.notification.CallIntent 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.LocalEventSendState @@ -1170,7 +1168,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemRtcNotificationContent(callIntent = CallIntent.VIDEO, state = RtcNotificationState.Started), + content = TimelineItemRtcNotificationContent(), ) 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 140d4f8ea3..384b78471d 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,19 +11,12 @@ 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 @@ -52,7 +45,6 @@ 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 @@ -79,7 +71,6 @@ 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) @@ -557,288 +548,10 @@ 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.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, - ) - reopenedState.imageEditorState.edits.cropRect.assertIsSimilarTo(rotatedCropRect) - assertThat(reopenedState.imageEditorState.edits.rotationQuarterTurns).isEqualTo(3) - assertThat(reopenedState.imageEditorState.edits.rotationDegrees).isEqualTo(270) - } - } - - 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(), @@ -860,18 +573,9 @@ 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, sendAsFile = sendAsFile), + attachment = aMediaAttachment(localMedia), onDoneListener = onDoneListener, mediaSenderFactory = MediaSenderFactory { timelineMode -> DefaultMediaSender( @@ -885,33 +589,15 @@ 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 deleted file mode 100644 index 43c893dd05..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditsTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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() - } -} 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 deleted file mode 100644 index f9db4f35f9..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/DefaultAttachmentImageEditorTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 deleted file mode 100644 index c70c6169e1..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/NormalizedCropRectTest.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 e06f4ad4cd..106fff7375 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 @@ -210,88 +210,19 @@ 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 deleted file mode 100644 index d3864794c2..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/VideoCompressionPresetSelectorTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 0ee342513a..24779ba78a 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,15 +6,12 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.matrix.api.core.UserId @@ -24,15 +21,19 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class IdentityChangeStateViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `show and resolve pin violation`() = runAndroidComposeUiTest { + fun `show and resolve pin violation`() { val eventsRecorder = EventsRecorder() - setIdentityChangeStateView( + rule.setIdentityChangeStateView( state = anIdentityChangeState( listOf( RoomMemberIdentityStateChange( @@ -44,18 +45,18 @@ class IdentityChangeStateViewTest { ), ) - 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.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") - clickOn(res = CommonStrings.action_dismiss) + rule.clickOn(res = CommonStrings.action_dismiss) eventsRecorder.assertSingle(IdentityChangeEvent.PinIdentity(UserId("@alice:localhost"))) } @Test - fun `show and resolve verification violation`() = runAndroidComposeUiTest { + fun `show and resolve verification violation`() { val eventsRecorder = EventsRecorder() - setIdentityChangeStateView( + rule.setIdentityChangeStateView( state = anIdentityChangeState( listOf( RoomMemberIdentityStateChange( @@ -67,17 +68,17 @@ class IdentityChangeStateViewTest { ), ) - 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.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") - clickOn(res = CommonStrings.crypto_identity_change_withdraw_verification_action) + rule.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`() = runAndroidComposeUiTest { - setIdentityChangeStateView( + fun `Should not show any banner if no violations`() { + rule.setIdentityChangeStateView( state = anIdentityChangeState( listOf( RoomMemberIdentityStateChange( @@ -92,10 +93,10 @@ class IdentityChangeStateViewTest { ), ) - onNodeWithText("identity was reset", substring = true).assertDoesNotExist() + rule.onNodeWithText("identity was reset", substring = true).assertDoesNotExist() } - private fun AndroidComposeUiTest.setIdentityChangeStateView( + private fun AndroidComposeTestRule.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 07a0fd5f94..02767fbeb9 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,53 +6,54 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn 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() + @Test - fun `clicking on resolve and resend emit the expected event`() = runAndroidComposeUiTest { + fun `clicking on resolve and resend emit the expected event`() { val eventsRecorder = EventsRecorder() - setResolveVerifiedUserSendFailureView( + rule.setResolveVerifiedUserSendFailureView( state = aResolveVerifiedUserSendFailureState( verifiedUserSendFailure = aChangedIdentitySendFailure(), eventSink = eventsRecorder, ), ) - clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on retry emit the expected event`() { val eventsRecorder = EventsRecorder() - setResolveVerifiedUserSendFailureView( + rule.setResolveVerifiedUserSendFailureView( state = aResolveVerifiedUserSendFailureState( verifiedUserSendFailure = aChangedIdentitySendFailure(), eventSink = eventsRecorder, ), ) - clickOn(res = CommonStrings.action_retry) + rule.clickOn(res = CommonStrings.action_retry) eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.Retry) } - private fun AndroidComposeUiTest.setResolveVerifiedUserSendFailureView( + private fun AndroidComposeTestRule.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 1dde33714f..77207d6b52 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,7 +11,6 @@ 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, sendAsFile: Boolean = false) = Attachment.Media( +fun aMediaAttachment(localMedia: LocalMedia) = Attachment.Media( localMedia = localMedia, - sendAsFile = sendAsFile, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt index fb39e67fc5..2f87af3df0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt @@ -37,7 +37,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation -import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope @@ -73,8 +72,6 @@ internal fun aTimelineItemContentFactory( failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(), failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(), sessionId = matrixClient.sessionId, - dateFormatter = FakeDateFormatter(), - stringProvider = FakeStringProvider(), ) internal fun TestScope.aTimelineItemsFactory( 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 b656430466..e198ea9043 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.ui.strings.CommonStrings @@ -22,46 +19,51 @@ 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.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() + @Test - fun `clicking on cancel emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on cancel emits the expected event`() { val eventsRecorder = EventsRecorder() - setLinkView( + rule.setLinkView( aLinkState( linkClick = ConfirmingLinkClick(aLink), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle( LinkEvent.Cancel ) } @Test - fun `clicking on continue emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on continue emits the expected event`() { val eventsRecorder = EventsRecorder() - setLinkView( + rule.setLinkView( aLinkState( linkClick = ConfirmingLinkClick(aLink), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle( LinkEvent.Confirm ) } @Test - fun `success state invokes the callback and emits the expected event`() = runAndroidComposeUiTest { + fun `success state invokes the callback and emits the expected event`() { val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(aLink) { callback -> - setLinkView( + rule.setLinkView( aLinkState( linkClick = AsyncAction.Success(aLink), eventSink = eventsRecorder, @@ -75,7 +77,7 @@ class LinkViewTest { } } -private fun AndroidComposeUiTest.setLinkView( +private fun AndroidComposeTestRule.setLinkView( state: LinkState, onLinkValid: (Link) -> Unit = EnsureNeverCalledWithParam(), ) { 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 e8d106a80f..7a2cc1110a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -1066,7 +1066,7 @@ class MessageComposerPresenterTest { ) givenRoomInfo( aRoomInfo( - isDm = true, + isDirect = true, activeMembersCount = 2, ) ) 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 546731ff87..2c33e348c0 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings @@ -25,45 +22,49 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PinnedMessagesBannerViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on the banner invoke expected callback`() = runAndroidComposeUiTest { + fun `clicking on the banner invoke expected callback`() { val eventsRecorder = EventsRecorder() val state = aLoadedPinnedMessagesBannerState( eventSink = eventsRecorder ) val pinnedEventId = state.currentPinnedMessage.eventId ensureCalledOnceWithParam(pinnedEventId) { callback -> - setPinnedMessagesBannerView( + rule.setPinnedMessagesBannerView( state = state, onClick = callback ) - onRoot().performClick() + rule.onRoot().performClick() eventsRecorder.assertSingle(PinnedMessagesBannerEvent.MoveToNextPinned) } } @Test - fun `clicking on view all emit the expected event`() = runAndroidComposeUiTest { + fun `clicking on view all emit the expected event`() { val eventsRecorder = EventsRecorder(expectEvents = true) val state = aLoadedPinnedMessagesBannerState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - setPinnedMessagesBannerView( + rule.setPinnedMessagesBannerView( state = state, onViewAllClick = callback ) - clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title) + rule.clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title) } } } -private fun AndroidComposeUiTest.setPinnedMessagesBannerView( +private fun AndroidComposeTestRule.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 9c10abb631..41671b71c1 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,19 +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.list import androidx.activity.ComponentActivity -import androidx.compose.ui.test.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.messages.impl.actionlist.ActionListEvent import io.element.android.features.messages.impl.actionlist.anActionListState @@ -34,28 +31,33 @@ import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack 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() + @Test - fun `clicking on back calls the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back calls the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aLoadedPinnedMessagesListState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - setPinnedMessagesListView( + rule.setPinnedMessagesListView( state = state, onBackClick = callback ) - pressBack() + rule.pressBack() } } @Test - fun `click on an event calls the expected callback`() = runAndroidComposeUiTest { + fun `click on an event calls the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val content = aTimelineItemFileContent() val state = aLoadedPinnedMessagesListState( @@ -65,16 +67,16 @@ class PinnedMessagesListViewTest { val event = state.timelineItems.first() as TimelineItem.Event ensureCalledOnceWithParam(event) { callback -> - setPinnedMessagesListView( + rule.setPinnedMessagesListView( state = state, onEventClick = callback ) - onAllNodesWithText(content.filename).onFirst().performClick() + rule.onAllNodesWithText(content.filename).onFirst().performClick() } } @Test - fun `long click on an event emits the expected event`() = runAndroidComposeUiTest { + fun `long click on an event emits the expected event`() { val eventsRecorder = EventsRecorder(expectEvents = true) val content = aTimelineItemFileContent() val state = aLoadedPinnedMessagesListState( @@ -82,10 +84,10 @@ class PinnedMessagesListViewTest { actionListState = anActionListState(eventSink = eventsRecorder) ) - setPinnedMessagesListView( + rule.setPinnedMessagesListView( state = state, ) - onAllNodesWithText(content.filename).onFirst() + rule.onAllNodesWithText(content.filename).onFirst() .performTouchInput { longClick() } @@ -94,7 +96,7 @@ class PinnedMessagesListViewTest { } } -private fun AndroidComposeUiTest.setPinnedMessagesListView( +private fun AndroidComposeTestRule.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 9e98f0fa49..315d9c459c 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,14 +6,11 @@ * 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.ExperimentalTestApi -import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.compose.ui.test.junit4.createComposeRule import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter import io.element.android.libraries.core.extensions.runCatchingExceptions @@ -21,12 +18,15 @@ 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 org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class DefaultHtmlConverterProviderTest { + @get:Rule val composeTestRule = createComposeRule() + private val provider = DefaultHtmlConverterProvider( mentionSpanProvider = MentionSpanProvider( permalinkParser = FakePermalinkParser(), @@ -43,8 +43,8 @@ class DefaultHtmlConverterProviderTest { } @Test - fun `calling provide after calling Update first should return an HtmlConverter`() = runComposeUiTest { - setContent { + fun `calling provide after calling Update first should return an HtmlConverter`() { + composeTestRule.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 af37fb61ef..194694714b 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,7 +10,6 @@ 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 @@ -1013,7 +1012,6 @@ class TimelinePresenterTest { sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), - liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), @@ -1032,7 +1030,6 @@ 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 2138d4ced2..c05625e2d3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -6,18 +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 import androidx.activity.ComponentActivity -import androidx.compose.ui.test.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.performClick import androidx.compose.ui.test.performScrollToIndex -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.messages.impl.timeline.components.MessageShieldData import io.element.android.features.messages.impl.timeline.components.aCriticalShield @@ -42,15 +39,19 @@ 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() + @Test - fun `reaching the end of the timeline with more events to load emits a LoadMore event`() = runAndroidComposeUiTest { + fun `reaching the end of the timeline with more events to load emits a LoadMore event`() { val eventsRecorder = EventsRecorder() - setTimelineView( + rule.setTimelineView( state = aTimelineState( timelineItems = persistentListOf( TimelineItem.Virtual( @@ -65,9 +66,9 @@ class TimelineViewTest { } @Test - fun `reaching the end of the timeline does not send a LoadMore event`() = runAndroidComposeUiTest { + fun `reaching the end of the timeline does not send a LoadMore event`() { val eventsRecorder = EventsRecorder() - setTimelineView( + rule.setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), eventSink = eventsRecorder, @@ -77,9 +78,9 @@ class TimelineViewTest { } @Test - fun `scroll to bottom on live timeline does not emit the Event`() = runAndroidComposeUiTest { + fun `scroll to bottom on live timeline does not emit the Event`() { val eventsRecorder = EventsRecorder() - setTimelineView( + rule.setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = true, @@ -91,14 +92,14 @@ class TimelineViewTest { eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.clear() - val contentDescription = activity!!.getString(CommonStrings.a11y_jump_to_bottom) - onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) + rule.onNodeWithContentDescription(contentDescription).performClick() } @Test - fun `scroll to bottom on detached timeline emits the expected Event`() = runAndroidComposeUiTest { + fun `scroll to bottom on detached timeline emits the expected Event`() { val eventsRecorder = EventsRecorder() - setTimelineView( + rule.setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = false, @@ -109,15 +110,15 @@ class TimelineViewTest { eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.clear() - val contentDescription = activity!!.getString(CommonStrings.a11y_jump_to_bottom) - onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) + rule.onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertSingle(TimelineEvent.JumpToLive) } @Test - fun `an empty timeline triggers a prefetch`() = runAndroidComposeUiTest { + fun `an empty timeline triggers a prefetch`() { val eventsRecorder = EventsRecorder() - setTimelineView( + rule.setTimelineView( state = aTimelineState( timelineItems = persistentListOf(), eventSink = eventsRecorder, @@ -128,9 +129,9 @@ class TimelineViewTest { } @Test - fun `show shield dialog`() = runAndroidComposeUiTest { + fun `show shield dialog`() { val eventsRecorder = EventsRecorder() - setTimelineView( + rule.setTimelineView( state = aTimelineState( timelineItems = persistentListOf( aTimelineItemEvent( @@ -142,8 +143,8 @@ class TimelineViewTest { eventSink = eventsRecorder, ), ) - val contentDescription = activity!!.getString(CommonStrings.a11y_encryption_details) - onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = rule.activity.getString(CommonStrings.a11y_encryption_details) + rule.onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertList( listOf( TimelineEvent.OnScrollFinished(0), @@ -153,9 +154,9 @@ class TimelineViewTest { } @Test - fun `hide shield dialog`() = runAndroidComposeUiTest { + fun `hide shield dialog`() { val eventsRecorder = EventsRecorder() - setTimelineView( + rule.setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = false, @@ -166,16 +167,16 @@ class TimelineViewTest { eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.clear() - clickOn(CommonStrings.action_ok) + rule.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`() = runAndroidComposeUiTest { + fun `scrolling near to the start of the loaded items triggers a pre-fetch`() { val eventsRecorder = EventsRecorder() val items = List(200) { aTimelineItemEvent( @@ -184,7 +185,7 @@ class TimelineViewTest { ) }.toImmutableList() - setTimelineView( + rule.setTimelineView( state = aTimelineState( timelineItems = items, eventSink = eventsRecorder, @@ -193,9 +194,9 @@ class TimelineViewTest { ), ) - onNodeWithTag("timeline").performScrollToIndex(180) + rule.onNodeWithTag("timeline").performScrollToIndex(180) - mainClock.advanceTimeBy(1000) + rule.mainClock.advanceTimeBy(1000) eventsRecorder.assertList( listOf( @@ -206,7 +207,7 @@ class TimelineViewTest { } } -private fun AndroidComposeUiTest.setTimelineView( +private fun AndroidComposeTestRule.setTimelineView( state: TimelineState, timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(), @@ -218,6 +219,7 @@ private fun AndroidComposeUiTest.setTimelineView( onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(), onMoreReactionsClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onReadReceiptClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(), forceJumpToBottomVisibility: Boolean = false, ) { setSafeContent(clearAndroidUiDispatcher = true) { @@ -233,6 +235,7 @@ private fun AndroidComposeUiTest.setTimelineView( onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, onReadReceiptClick = onReadReceiptClick, + onJoinCallClick = onJoinCallClick, forceJumpToBottomVisibility = forceJumpToBottomVisibility, ) } 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 40671e4bf8..64b5216d2e 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,15 +6,12 @@ * 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent @@ -23,11 +20,14 @@ 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 org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class TimelineItemPollViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test fun `answering a poll with first answer should emit a PollAnswerSelected event`() { testAnswer(answerIndex = 0) @@ -38,17 +38,17 @@ class TimelineItemPollViewTest { testAnswer(answerIndex = 1) } - private fun testAnswer(answerIndex: Int) = runAndroidComposeUiTest { + private fun testAnswer(answerIndex: Int) { val eventsRecorder = EventsRecorder() val content = aTimelineItemPollContent() - setContent { + rule.setContent { TimelineItemPollView( content = content, eventSink = eventsRecorder ) } val answer = content.answerItems[answerIndex].answer - onNode( + rule.onNode( matcher = hasText(answer.text), useUnmergedTree = true, ).performClick() @@ -56,38 +56,38 @@ class TimelineItemPollViewTest { } @Test - fun `editing a poll should emit a PollEditClicked event`() = runAndroidComposeUiTest { + fun `editing a poll should emit a PollEditClicked event`() { val eventsRecorder = EventsRecorder() val content = aTimelineItemPollContent( isMine = true, isEditable = true, ) - setContent { + rule.setContent { TimelineItemPollView( content = content, eventSink = eventsRecorder ) } - clickOn(CommonStrings.action_edit_poll) + rule.clickOn(CommonStrings.action_edit_poll) eventsRecorder.assertSingle(TimelineEvent.EditPoll(content.eventId!!)) } @Test - fun `closing a poll should emit a PollEndClicked event`() = runAndroidComposeUiTest { + fun `closing a poll should emit a PollEndClicked event`() { val eventsRecorder = EventsRecorder() val content = aTimelineItemPollContent( isMine = true, ) - setContent { + rule.setContent { TimelineItemPollView( content = content, eventSink = eventsRecorder ) } - clickOn(CommonStrings.action_end_poll) + rule.clickOn(CommonStrings.action_end_poll) // A confirmation dialog should be shown eventsRecorder.assertEmpty() - pressTag(TestTags.dialogPositive.value) + rule.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 7b8597f05a..154225aa7a 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,14 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -41,40 +38,45 @@ import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder 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() + 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`() = runAndroidComposeUiTest { + fun `getTextWithResolvedMentions - does nothing for a non spannable CharSequence`() = runTest { val charSequence = "Hello @alice:example.com" val mentionSpanUpdater = aMentionSpanUpdater() - val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) assertThat(result.getMentionSpans()).isEmpty() assert(formatLambda).isNeverCalled() } @Test - fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runAndroidComposeUiTest { + fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runTest { val charSequence = SpannableString("Hello @alice:example.com") val mentionSpanUpdater = aMentionSpanUpdater() - val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) assertThat(result.getMentionSpans()).isEmpty() assert(formatLambda).isNeverCalled() } @Test - fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runAndroidComposeUiTest { + fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runTest { val charSequence = "Hello @alice:example.com" val mentionSpanUpdater = aMentionSpanUpdater() - val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null)) + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null)) assertThat(result.getMentionSpans()).isEmpty() assertThat(result.toString()).isEqualTo(charSequence) @@ -82,7 +84,7 @@ class TimelineTextViewTest { } @Test - fun `getTextWithResolvedMentions - with Room mention format correctly`() = runAndroidComposeUiTest { + fun `getTextWithResolvedMentions - with Room mention format correctly`() = runTest { val mentionType = MentionType.Room(roomIdOrAlias = A_ROOM_ID_2.toRoomIdOrAlias()) val charSequence = buildSpannedString { append("Hello ") @@ -91,7 +93,7 @@ class TimelineTextViewTest { } } val mentionSpanUpdater = aMentionSpanUpdater() - val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val expectedDisplayText = mentionType.toString() assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) @@ -100,7 +102,7 @@ class TimelineTextViewTest { } @Test - fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runAndroidComposeUiTest { + fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runTest { val mentionType = MentionType.User(userId = A_USER_ID) val charSequence = buildSpannedString { append("Hello ") @@ -109,7 +111,7 @@ class TimelineTextViewTest { } } val mentionSpanUpdater = aMentionSpanUpdater() - val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val expectedDisplayText = mentionType.toString() assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) @@ -117,7 +119,7 @@ class TimelineTextViewTest { } @Test - fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runAndroidComposeUiTest { + fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runTest { val mentionType = MentionType.User(userId = A_USER_ID) val charSequence = buildSpannedString { append("Hello ") @@ -127,12 +129,12 @@ class TimelineTextViewTest { } val mentionSpanUpdater = aMentionSpanUpdater() val expectedDisplayText = mentionType.toString() - val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) assert(formatLambda).isCalledOnce() } - private suspend fun AndroidComposeUiTest.getText( + private suspend fun AndroidComposeTestRule.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..957b01d1ed 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 @@ -78,7 +78,9 @@ import org.robolectric.RobolectricTestRunner import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes -@Suppress("LargeClass") @RunWith(RobolectricTestRunner::class) class TimelineItemContentMessageFactoryTest { +@Suppress("LargeClass") +@RunWith(RobolectricTestRunner::class) +class TimelineItemContentMessageFactoryTest { @Test fun `test create OtherMessageType`() = runTest { val sut = createTimelineItemContentMessageFactory() @@ -108,9 +110,11 @@ import kotlin.time.Duration.Companion.minutes eventId = AN_EVENT_ID, ) val expected = TimelineItemLocationContent( + body = "body", + location = Location(lat = 1.0, lon = 2.0, accuracy = null), description = "description", assetType = assetType, - mode = TimelineItemLocationContent.Mode.Static(location = Location(lat = 1.0, lon = 2.0, accuracy = null)), + mode = TimelineItemLocationContent.Mode.Static, senderId = A_USER_ID, senderProfile = aProfileDetails(), ) @@ -162,11 +166,16 @@ import kotlin.time.Duration.Companion.minutes senderProfile = aProfileDetails(), eventId = AN_EVENT_ID, ) as TimelineItemTextContent - val expected = TimelineItemTextContent(body = "https://www.example.org", htmlDocument = null, isEdited = false, formattedBody = buildSpannedString { - inSpans(URLSpan("https://www.example.org")) { - append("https://www.example.org") + val expected = TimelineItemTextContent( + body = "https://www.example.org", + htmlDocument = null, + isEdited = false, + formattedBody = buildSpannedString { + inSpans(URLSpan("https://www.example.org")) { + append("https://www.example.org") + } } - }) + ) assertThat(result.body).isEqualTo(expected.body) assertThat(result.htmlDocument).isEqualTo(expected.htmlDocument) assertThat(result.plainText).isEqualTo(expected.plainText) @@ -191,7 +200,9 @@ import kotlin.time.Duration.Companion.minutes append("and manually added link") } }.toSpannable() - val sut = createTimelineItemContentMessageFactory(domConverterTransform = { expected }) + val sut = createTimelineItemContentMessageFactory( + domConverterTransform = { expected } + ) val result = sut.create( content = createMessageContent( type = TextMessageType( @@ -208,7 +219,9 @@ import kotlin.time.Duration.Companion.minutes @Test fun `test create TextMessageType with unknown formatted body does nothing`() = runTest { - val sut = createTimelineItemContentMessageFactory(htmlConverterTransform = { it }) + val sut = createTimelineItemContentMessageFactory( + htmlConverterTransform = { it } + ) val result = sut.create( content = createMessageContent( type = TextMessageType( @@ -343,10 +356,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 +597,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 8050278fb2..af3acee6a2 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,55 +6,56 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ProtectedViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `when hideContent is false, the content is rendered`() = runAndroidComposeUiTest { - setProtectedView( + fun `when hideContent is false, the content is rendered`() { + rule.setProtectedView( hideContent = false, content = { Text("Hello") } ) - onNodeWithText("Hello").assertExists() + rule.onNodeWithText("Hello").assertExists() } @Test - fun `when hideContent is true, the content is not rendered, and user can reveal it`() = runAndroidComposeUiTest { + fun `when hideContent is true, the content is not rendered, and user can reveal it`() { ensureCalledOnce { - setProtectedView( + rule.setProtectedView( hideContent = true, onShowClick = it, content = { Text("Hello") } ) - onNodeWithText("Hello").assertDoesNotExist() - clickOn(CommonStrings.action_show) + rule.onNodeWithText("Hello").assertDoesNotExist() + rule.clickOn(CommonStrings.action_show) } } } -private fun AndroidComposeUiTest.setProtectedView( +private fun AndroidComposeTestRule.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/utils/DefaultMessageSummaryFormatterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultMessageSummaryFormatterTest.kt deleted file mode 100644 index 664d21ed64..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultMessageSummaryFormatterTest.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 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 DefaultMessageSummaryFormatterTest { - 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/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt index 7d3b6cf647..9c55bf3a85 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt @@ -24,7 +24,6 @@ import io.element.android.libraries.audio.api.AudioFocusRequester import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.timeline.FakeTimeline @@ -47,9 +46,7 @@ import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.lambda.any 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.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -62,7 +59,6 @@ import java.io.File import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -@Suppress("LargeClass") class DefaultVoiceMessageComposerPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -410,37 +406,6 @@ class DefaultVoiceMessageComposerPresenterTest { } } - @Test - fun `present - send voice message passes reply event ID only when in reply mode`() = runTest { - val presenter = createDefaultVoiceMessageComposerPresenter() - presenter.test { - // First send in Normal mode (default composerMode). - awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage) - assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState()) - val idleAfterFirstSend = awaitItem() - assertThat(idleAfterFirstSend.voiceMessageState).isEqualTo(VoiceMessageState.Idle) - - // Switching to reply mode does not trigger recomposition, so reuse the prior eventSink. - messageComposerContext.composerMode = aReplyMode() - idleAfterFirstSend.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage) - assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState()) - val finalState = awaitItem() - assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) - - sendVoiceMessageResult.assertions().isCalledExactly(2) - .withSequence( - listOf(any(), any(), any(), value(null)), - listOf(any(), any(), any(), value(AN_EVENT_ID)), - ) - - testPauseAndDestroy(finalState) - } - } - @Test fun `present - send while playing`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() diff --git a/features/messages/test/build.gradle.kts b/features/messages/test/build.gradle.kts index f89dd8de06..b839f8de06 100644 --- a/features/messages/test/build.gradle.kts +++ b/features/messages/test/build.gradle.kts @@ -16,7 +16,6 @@ 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 fff3ede5d3..02a6918ad8 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, sendAsFile: Boolean): MediaOptimizationSelectorPresenter { + override fun create(localMedia: LocalMedia): MediaOptimizationSelectorPresenter { return fakePresenter } } diff --git a/features/poll/api/src/main/res/values-vi/translations.xml b/features/poll/api/src/main/res/values-vi/translations.xml index 9c94ec71f6..21651828df 100644 --- a/features/poll/api/src/main/res/values-vi/translations.xml +++ b/features/poll/api/src/main/res/values-vi/translations.xml @@ -1,8 +1,5 @@ - - "%1$d phần trăm tổng số phiếu bầu" - "Xóa lựa chọn trước đó" "Đây là câu trả lời chiến thắng" diff --git a/features/poll/api/src/main/res/values-zh/translations.xml b/features/poll/api/src/main/res/values-zh/translations.xml index 037d6d7c85..773d2b03fc 100644 --- a/features/poll/api/src/main/res/values-zh/translations.xml +++ b/features/poll/api/src/main/res/values-zh/translations.xml @@ -4,5 +4,5 @@ "%1$d 总投票百分比" "将移除之前的选择" - "此为胜出的答案" + "这是获胜的答案" diff --git a/features/poll/impl/src/main/res/values-ca/translations.xml b/features/poll/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 21385e6473..0000000000 --- a/features/poll/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - "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/main/res/values-vi/translations.xml b/features/poll/impl/src/main/res/values-vi/translations.xml index b56fe7039d..dc29666c74 100644 --- a/features/poll/impl/src/main/res/values-vi/translations.xml +++ b/features/poll/impl/src/main/res/values-vi/translations.xml @@ -5,7 +5,6 @@ "Ẩn phiếu" "Lựa chọn %1$d" "Các thay đổi của bạn chưa được lưu. Bạn có chắc muốn quay lại không?" - "Tùy chọn xóa %1$s" "Câu hỏi hoặc chủ đề" "Cuộc thăm dò này về vấn đề gì?" "Tạo Cuộc thăm dò ý kiến" diff --git a/features/poll/impl/src/main/res/values-zh/translations.xml b/features/poll/impl/src/main/res/values-zh/translations.xml index ee40168ea7..f231e99d76 100644 --- a/features/poll/impl/src/main/res/values-zh/translations.xml +++ b/features/poll/impl/src/main/res/values-zh/translations.xml @@ -5,11 +5,11 @@ "隐藏投票" "选项 %1$d" "更改尚未保存,确定要返回吗?" - "删除选项 %1$s" + "删除选项%1$s" "问题或话题" "投票的内容是什么?" "创建投票" - "你确定要删除此投票?" + "您确定要删除此投票吗?" "删除投票" "编辑投票" "无法找到正在进行的投票。" 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 a6b97c554c..1ff25a0a81 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.poll.api.pollcontent.aPollContentState import io.element.android.features.poll.impl.R @@ -29,29 +26,34 @@ 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 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() + @Test - fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setPollHistoryViewView( + rule.setPollHistoryViewView( aPollHistoryState( eventSink = eventsRecorder ), goBack = it ) - pressBack() + rule.pressBack() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on edit poll invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on edit poll invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) val eventId = EventId("\$anEventId") val state = aPollHistoryState( @@ -67,17 +69,17 @@ class PollHistoryViewTest { eventSink = eventsRecorder ) ensureCalledOnceWithParam(eventId) { - setPollHistoryViewView( + rule.setPollHistoryViewView( state = state, onEditPoll = it ) - clickOn(CommonStrings.action_edit_poll) + rule.clickOn(CommonStrings.action_edit_poll) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on poll end emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on poll end emits the expected Event`() { val eventsRecorder = EventsRecorder() val eventId = EventId("\$anEventId") val state = aPollHistoryState( @@ -93,16 +95,16 @@ class PollHistoryViewTest { ), eventSink = eventsRecorder ) - setPollHistoryViewView( + rule.setPollHistoryViewView( state = state, ) - clickOn(CommonStrings.action_end_poll) + rule.clickOn(CommonStrings.action_end_poll) // Cancel the dialog - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) // Do it again, and confirm the dialog - clickOn(CommonStrings.action_end_poll) + rule.clickOn(CommonStrings.action_end_poll) eventsRecorder.assertEmpty() - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle( PollHistoryEvents.EndPoll(eventId) ) @@ -110,7 +112,7 @@ class PollHistoryViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on poll answer emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on poll answer emits the expected Event`() { val eventsRecorder = EventsRecorder() val eventId = EventId("\$anEventId") val state = aPollHistoryState( @@ -127,10 +129,10 @@ class PollHistoryViewTest { eventSink = eventsRecorder ) val answer = state.pollHistoryItems.ongoing.first().state.answerItems.first().answer - setPollHistoryViewView( + rule.setPollHistoryViewView( state = state, ) - onNodeWithText( + rule.onNodeWithText( text = answer.text, useUnmergedTree = true, ).performClick() @@ -140,14 +142,14 @@ class PollHistoryViewTest { } @Test - fun `clicking on past tab emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on past tab emits the expected Event`() { val eventsRecorder = EventsRecorder() - setPollHistoryViewView( + rule.setPollHistoryViewView( aPollHistoryState( eventSink = eventsRecorder ), ) - clickOn(R.string.screen_polls_history_filter_past) + rule.clickOn(R.string.screen_polls_history_filter_past) eventsRecorder.assertSingle( PollHistoryEvents.SelectFilter(filter = PollHistoryFilter.PAST) ) @@ -155,22 +157,22 @@ class PollHistoryViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on load more emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on load more emits the expected Event`() { val eventsRecorder = EventsRecorder() - setPollHistoryViewView( + rule.setPollHistoryViewView( aPollHistoryState( hasMoreToLoad = true, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_load_more) + rule.clickOn(CommonStrings.action_load_more) eventsRecorder.assertSingle( PollHistoryEvents.LoadMore ) } } -private fun AndroidComposeUiTest.setPollHistoryViewView( +private fun AndroidComposeTestRule.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 d0adc8e94f..a3779809d7 100644 --- a/features/poll/test/build.gradle.kts +++ b/features/poll/test/build.gradle.kts @@ -15,7 +15,6 @@ 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 1c801dbbad..ad28c90966 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -51,7 +51,6 @@ 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) @@ -69,7 +68,6 @@ 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) @@ -116,7 +114,6 @@ 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 ef27c499af..b3fb68fe05 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,5 +20,4 @@ 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 96b1cddc94..e58706e9fe 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,14 +10,12 @@ 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) @@ -30,12 +28,10 @@ class AdvancedSettingsNode( @Composable override fun View(modifier: Modifier) { val state = presenter.present() - val context = LocalContext.current AdvancedSettingsView( state = state, modifier = modifier, - onBackClick = ::navigateUp, - onOpenAppSettingsClick = context::openAppSettingsPage + onBackClick = ::navigateUp ) } } 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 45c136ab62..c2871e0be8 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 @@ -23,7 +23,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine @@ -46,17 +45,10 @@ class AdvancedSettingsPresenter( val isSharePresenceEnabled by remember { sessionPreferencesStore.isSharePresenceEnabled() }.collectAsState(initial = true) - val isBlackThemeAllowed by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.AllowBlackTheme) - }.collectAsState(initial = false) - val theme = remember(isBlackThemeAllowed) { - appPreferencesStore.getThemeFlow().mapToTheme(isBlackThemeAllowed) + val theme = remember { + appPreferencesStore.getThemeFlow().mapToTheme() }.collectAsState(initial = Theme.System) - val liveLocationMinimumDistanceUpdate by produceState(null) { - appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow().collect { value = it } - } - val mediaPreviewConfigState = mediaPreviewConfigStateStore.state() val themeOption by remember { @@ -64,7 +56,6 @@ class AdvancedSettingsPresenter( when (theme.value) { Theme.System -> ThemeOption.System Theme.Dark -> ThemeOption.Dark - Theme.Black -> ThemeOption.Black Theme.Light -> ThemeOption.Light } } @@ -74,14 +65,6 @@ class AdvancedSettingsPresenter( value = featureFlagService.isFeatureEnabled(FeatureFlags.SelectableMediaQuality) } - val availableThemeOptions = remember(isBlackThemeAllowed) { - if (isBlackThemeAllowed) { - ThemeOption.entries - } else { - ThemeOption.entries.filterNot { it == ThemeOption.Black } - }.toImmutableList() - } - val mediaOptimizationState by produceState(null) { val hasSplitMediaQualityOptionsFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.SelectableMediaQuality) combine( @@ -115,15 +98,11 @@ class AdvancedSettingsPresenter( when (event.theme) { ThemeOption.System -> appPreferencesStore.setTheme(Theme.System.name) ThemeOption.Dark -> appPreferencesStore.setTheme(Theme.Dark.name) - ThemeOption.Black -> appPreferencesStore.setTheme(Theme.Black.name) ThemeOption.Light -> appPreferencesStore.setTheme(Theme.Light.name) } } 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) } @@ -138,9 +117,7 @@ class AdvancedSettingsPresenter( isSharePresenceEnabled = isSharePresenceEnabled, mediaOptimizationState = mediaOptimizationState, 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 94019c1f01..6eb7414a29 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,19 +11,16 @@ 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 kotlinx.collections.immutable.ImmutableList +import io.element.android.libraries.ui.strings.CommonStrings data class AdvancedSettingsState( val isDeveloperModeEnabled: Boolean, val isSharePresenceEnabled: Boolean, val mediaOptimizationState: MediaOptimizationState?, val theme: ThemeOption, - val availableThemeOptions: ImmutableList, val mediaPreviewConfigState: MediaPreviewConfigState, - val liveLocationMinimumDistanceUpdate: Int?, val eventSink: (AdvancedSettingsEvents) -> Unit ) @@ -44,24 +41,16 @@ enum class ThemeOption : DropdownOption { System { @Composable @ReadOnlyComposable - override fun getText(): String = stringResource(R.string.theme_system) + override fun getText(): String = stringResource(CommonStrings.common_system) }, - - Light { - @Composable - @ReadOnlyComposable - override fun getText(): String = stringResource(R.string.theme_light) - }, - Dark { @Composable @ReadOnlyComposable - override fun getText(): String = stringResource(R.string.theme_dark) + override fun getText(): String = stringResource(CommonStrings.common_dark) }, - - Black { + Light { @Composable @ReadOnlyComposable - override fun getText(): String = stringResource(R.string.theme_black) + override fun getText(): String = stringResource(CommonStrings.common_light) } } 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 2df59e3ced..6cbe6e5c51 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 @@ -12,8 +12,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.preferences.api.store.VideoCompressionPreset -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList open class AdvancedSettingsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -38,10 +36,8 @@ fun aAdvancedSettingsState( isSharePresenceEnabled: Boolean = false, mediaOptimizationState: MediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = false), theme: ThemeOption = ThemeOption.System, - 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 = {}, @@ -50,13 +46,11 @@ fun aAdvancedSettingsState( isSharePresenceEnabled = isSharePresenceEnabled, mediaOptimizationState = mediaOptimizationState, theme = theme, - availableThemeOptions = availableThemeOptions, mediaPreviewConfigState = MediaPreviewConfigState( hideInviteAvatars = hideInviteAvatars, timelineMediaPreviewValue = timelineMediaPreviewValue, 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 af28e3443f..c2b51973d0 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,24 +8,15 @@ 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 @@ -37,17 +28,14 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.ElementPreviewBlack 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 @@ -58,13 +46,12 @@ 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 +import kotlinx.collections.immutable.toImmutableList @Composable fun AdvancedSettingsView( state: AdvancedSettingsState, onBackClick: () -> Unit, - onOpenAppSettingsClick: () -> Unit, modifier: Modifier = Modifier, ) { val analyticsService = LocalAnalyticsService.current @@ -87,7 +74,7 @@ fun AdvancedSettingsView( PreferenceDropdown( title = stringResource(id = CommonStrings.common_appearance), selectedOption = state.theme, - options = state.availableThemeOptions, + options = ThemeOption.entries.toImmutableList(), onSelectOption = { themeOption -> state.eventSink(AdvancedSettingsEvents.SetTheme(themeOption)) } @@ -203,15 +190,6 @@ fun AdvancedSettingsView( } ModerationAndSafety(state) - if (state.liveLocationMinimumDistanceUpdate != null) { - LiveLocationUpdatesSection( - value = state.liveLocationMinimumDistanceUpdate, - onValueSaved = { value -> - state.eventSink(AdvancedSettingsEvents.SetLiveLocationMinimumDistanceUpdate(value)) - }, - onOpenAppPermissionsClick = onOpenAppSettingsClick, - ) - } } } @@ -336,78 +314,6 @@ private fun ModerationAndSafety( } } -@Composable -private fun LiveLocationUpdatesSection( - value: Int, - onValueSaved: (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 = { - onValueSaved(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) = @@ -418,18 +324,12 @@ internal fun AdvancedSettingsViewLightPreview(@PreviewParameter(AdvancedSettings internal fun AdvancedSettingsViewDarkPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) = ElementPreviewDark { ContentToPreview(state) } -@PreviewWithLargeHeight -@Composable -internal fun AdvancedSettingsViewBlackPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) = - ElementPreviewBlack { ContentToPreview(state) } - @ExcludeFromCoverage @Composable private fun ContentToPreview(state: AdvancedSettingsState) { AdvancedSettingsView( state = state, - onBackClick = { }, - onOpenAppSettingsClick = {} + onBackClick = { } ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt index e7ad6e2284..4c76c6ec7e 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,7 +25,10 @@ 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 @@ -42,6 +45,7 @@ class AppDeveloperSettingsPresenter( private val featureFlagService: FeatureFlagService, private val rageshakePresenter: Presenter, private val appPreferencesStore: AppPreferencesStore, + private val buildMeta: BuildMeta, ) : Presenter { @Composable override fun present(): AppDeveloperSettingsState { @@ -67,6 +71,14 @@ 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 9b2a2aa3a9..71051cf829 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,6 +87,14 @@ 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/NotificationSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt index 12f13e734b..9d9e80b3f3 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 @@ -223,7 +223,7 @@ class NotificationSettingsPresenter( notificationSettingsService.setDefaultRoomNotificationMode( isEncrypted = encryptedGroupDefaultMode != RoomNotificationMode.ALL_MESSAGES, mode = RoomNotificationMode.ALL_MESSAGES, - isDM = false, + isOneToOne = false, ) } @@ -234,7 +234,7 @@ class NotificationSettingsPresenter( notificationSettingsService.setDefaultRoomNotificationMode( isEncrypted = encryptedOneToOneDefaultMode != RoomNotificationMode.ALL_MESSAGES, mode = RoomNotificationMode.ALL_MESSAGES, - isDM = true, + isOneToOne = true, ) } }.fold( 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 1357caeef3..96097983c9 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 isDm: Boolean + val isOneToOne: Boolean ) : NodeInputs private val callback: Callback = callback() private val inputs = inputs() - private val presenter = presenterFactory.create(inputs.isDm) + private val presenter = presenterFactory.create(inputs.isOneToOne) @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 f2cb8b02cc..178f7033f3 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 isDm: Boolean, + @Assisted private val isOneToOne: Boolean, private val roomListService: RoomListService, ) : Presenter { @AssistedFactory interface Factory { - fun create(isDm: Boolean): EditDefaultNotificationSettingPresenter + fun create(isOneToOne: Boolean): EditDefaultNotificationSettingPresenter } private val collator = Collator.getInstance().apply { @@ -86,7 +86,7 @@ class EditDefaultNotificationSettingPresenter( } return EditDefaultNotificationSettingState( - isOneToOne = isDm, + isOneToOne = isOneToOne, 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 = isDm).getOrThrow() + mode.value = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = isOneToOne).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.isDm == isDm + roomWithUserDefinedRules.contains(roomSummary.roomId) && roomSummary.isOneToOne == isOneToOne } .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, isDM = isDm) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne) .map { - notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isDM = isDm) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne) } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvent.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvents.kt similarity index 78% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvent.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvents.kt index 5a10a50ba6..be266869be 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvent.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvents.kt @@ -10,7 +10,7 @@ package io.element.android.features.preferences.impl.root import io.element.android.libraries.matrix.api.core.SessionId -sealed interface PreferencesRootEvent { - data object OnVersionInfoClick : PreferencesRootEvent - data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvent +sealed interface PreferencesRootEvents { + data object OnVersionInfoClick : PreferencesRootEvents + data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvents } 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 f407149007..3d6a829167 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,7 +19,6 @@ 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 @@ -31,6 +30,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.sessionstorage.api.SessionStore @@ -56,7 +56,6 @@ class PreferencesRootPresenter( private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val featureFlagService: FeatureFlagService, private val sessionStore: SessionStore, - private val sessionEnterpriseService: SessionEnterpriseService, ) : Presenter { @Composable override fun present(): PreferencesRootState { @@ -100,6 +99,9 @@ class PreferencesRootPresenter( val accountManagementUrl: MutableState = remember { mutableStateOf(null) } + val devicesManagementUrl: MutableState = remember { + mutableStateOf(null) + } var canDeactivateAccount by remember { mutableStateOf(false) } @@ -108,9 +110,9 @@ class PreferencesRootPresenter( canDeactivateAccount = matrixClient.canDeactivateAccount() } - val nbOfBlockedUsers by produceState(initialValue = 0) { + val showBlockedUsersItem by produceState(initialValue = false) { matrixClient.ignoredUsersFlow - .onEach { value = it.size } + .onEach { value = it.isNotEmpty() } .launchIn(this) } @@ -119,17 +121,17 @@ class PreferencesRootPresenter( val directLogoutState = directLogoutPresenter.present() LaunchedEffect(Unit) { - initAccountManagementUrl(accountManagementUrl) + initAccountManagementUrl(accountManagementUrl, devicesManagementUrl) } val showDeveloperSettings by showDeveloperSettingsProvider.showDeveloperSettings.collectAsState() - fun handleEvent(event: PreferencesRootEvent) { + fun handleEvent(event: PreferencesRootEvents) { when (event) { - is PreferencesRootEvent.OnVersionInfoClick -> { + is PreferencesRootEvents.OnVersionInfoClick -> { showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope) } - is PreferencesRootEvent.SwitchToSession -> coroutineScope.launch { + is PreferencesRootEvents.SwitchToSession -> coroutineScope.launch { sessionStore.setLatestSession(event.sessionId.value) } } @@ -144,12 +146,13 @@ class PreferencesRootPresenter( showSecureBackup = !canVerifyUserSession, showSecureBackupBadge = showSecureBackupIndicator, accountManagementUrl = accountManagementUrl.value, + devicesManagementUrl = devicesManagementUrl.value, showAnalyticsSettings = hasAnalyticsProviders, canReportBug = canReportBug, showLinkNewDevice = showLinkNewDevice, showDeveloperSettings = showDeveloperSettings, canDeactivateAccount = canDeactivateAccount, - nbOfBlockedUsers = nbOfBlockedUsers, + showBlockedUsersItem = showBlockedUsersItem, showLabsItem = showLabsItem, directLogoutState = directLogoutState, snackbarMessage = snackbarMessage, @@ -159,11 +162,9 @@ class PreferencesRootPresenter( private fun CoroutineScope.initAccountManagementUrl( accountManagementUrl: MutableState, + devicesManagementUrl: MutableState, ) = launch { - accountManagementUrl.value = matrixClient.getAccountManagementUrl(null) - .getOrNull() - ?.let { - sessionEnterpriseService.tweakMasUrl(it) - } + accountManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.Profile).getOrNull() + devicesManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.DevicesList).getOrNull() } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index 6474f07a69..d637ae6c87 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -23,16 +23,15 @@ data class PreferencesRootState( val showSecureBackup: Boolean, val showSecureBackupBadge: Boolean, val accountManagementUrl: String?, + val devicesManagementUrl: String?, val canReportBug: Boolean, val showLinkNewDevice: Boolean, val showAnalyticsSettings: Boolean, val showDeveloperSettings: Boolean, val canDeactivateAccount: Boolean, - val nbOfBlockedUsers: Int, + val showBlockedUsersItem: Boolean, val showLabsItem: Boolean, val directLogoutState: DirectLogoutState, val snackbarMessage: SnackbarMessage?, - val eventSink: (PreferencesRootEvent) -> Unit, -) { - val showBlockedUsersItem = nbOfBlockedUsers > 0 -} + val eventSink: (PreferencesRootEvents) -> Unit, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt index d53cd008e4..b8d1f1c2b6 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -8,103 +8,36 @@ package io.element.android.features.preferences.impl.root -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.DeviceId 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.ui.strings.CommonStrings import kotlinx.collections.immutable.toImmutableList -open class PreferencesRootStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - // Nominal state, that a regular user will see if multi account is enabled - aPreferencesRootState( - myUser = aMatrixUser(avatarUrl = "anAvatarUrl"), - version = "Version 1.1 (1)", - deviceId = DeviceId("ILAKNDNASDLK"), - isMultiAccountEnabled = true, - otherSessions = aMatrixUserList().drop(1).take(1), - showSecureBackup = true, - accountManagementUrl = "aUrl", - canReportBug = true, - showLinkNewDevice = true, - showAnalyticsSettings = true, - canDeactivateAccount = false, - nbOfBlockedUsers = 3, - showLabsItem = true, - ), - aPreferencesRootState( - myUser = aMatrixUser(displayName = null), - isMultiAccountEnabled = true, - showSecureBackup = true, - canDeactivateAccount = true, - ), - aPreferencesRootState( - isMultiAccountEnabled = true, - otherSessions = aMatrixUserList().drop(1).take(3), - accountManagementUrl = "aUrl", - showSecureBackup = true, - showSecureBackupBadge = true, - ), - aPreferencesRootState( - deviceId = DeviceId("ILAKNDNASDLK"), - showLabsItem = true, - canReportBug = true, - nbOfBlockedUsers = 3, - snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete), - ), - aPreferencesRootState( - showLinkNewDevice = true, - showAnalyticsSettings = true, - showDeveloperSettings = true, - canDeactivateAccount = true, - ), - // Minimal state - aPreferencesRootState(), - ) -} - fun aPreferencesRootState( myUser: MatrixUser = aMatrixUser(), - version: String = "Version 1.1 (1)", - deviceId: DeviceId? = null, - isMultiAccountEnabled: Boolean = false, otherSessions: List = emptyList(), - showSecureBackup: Boolean = false, - showSecureBackupBadge: Boolean = false, - accountManagementUrl: String? = null, - canReportBug: Boolean = false, - showLinkNewDevice: Boolean = false, - showAnalyticsSettings: Boolean = false, - showDeveloperSettings: Boolean = false, - canDeactivateAccount: Boolean = false, - nbOfBlockedUsers: Int = 0, - showLabsItem: Boolean = false, - directLogoutState: DirectLogoutState = aDirectLogoutState(), - snackbarMessage: SnackbarMessage? = null, - eventSink: (PreferencesRootEvent) -> Unit = {}, + eventSink: (PreferencesRootEvents) -> Unit = { _ -> }, ) = PreferencesRootState( myUser = myUser, - version = version, - deviceId = deviceId, - isMultiAccountEnabled = isMultiAccountEnabled, + version = "Version 1.1 (1)", + deviceId = DeviceId("ILAKNDNASDLK"), + isMultiAccountEnabled = true, otherSessions = otherSessions.toImmutableList(), - showSecureBackup = showSecureBackup, - showSecureBackupBadge = showSecureBackupBadge, - accountManagementUrl = accountManagementUrl, - canReportBug = canReportBug, - showLinkNewDevice = showLinkNewDevice, - showAnalyticsSettings = showAnalyticsSettings, - showDeveloperSettings = showDeveloperSettings, - canDeactivateAccount = canDeactivateAccount, - nbOfBlockedUsers = nbOfBlockedUsers, - showLabsItem = showLabsItem, - directLogoutState = directLogoutState, - snackbarMessage = snackbarMessage, + showSecureBackup = true, + showSecureBackupBadge = true, + accountManagementUrl = "aUrl", + devicesManagementUrl = "anOtherUrl", + showAnalyticsSettings = true, + showLinkNewDevice = true, + canReportBug = true, + showDeveloperSettings = true, + showBlockedUsersItem = true, + showLabsItem = true, + canDeactivateAccount = true, + snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete), + directLogoutState = aDirectLogoutState(), eventSink = eventSink, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 4afc2d27bf..5e3c9d6759 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -9,6 +9,7 @@ package io.element.android.features.preferences.impl.root import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -27,20 +28,23 @@ import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferencePage +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.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.HorizontalDivider 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.Text -import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.MatrixUserProvider import io.element.android.libraries.matrix.ui.components.MatrixUserRow +import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -78,24 +82,14 @@ fun PreferencesRootView( modifier = Modifier.clickable { onOpenUserProfile(state.myUser) }, - matrixUser = state.myUser, + user = state.myUser, ) if (state.isMultiAccountEnabled) { MultiAccountSection( state = state, onAddAccountClick = onAddAccountClick, ) - } else { - HorizontalDivider() } - // User status will be added here - // 'Account' section - ManageAccountSection( - state = state, - onManageAccountClick = onManageAccountClick, - onLinkNewDeviceClick = onLinkNewDeviceClick, - onOpenBlockedUsers = onOpenBlockedUsers - ) // 'Manage my app' section ManageAppSection( state = state, @@ -104,6 +98,14 @@ fun PreferencesRootView( onSecureBackupClick = onSecureBackupClick, ) + // 'Account' section + ManageAccountSection( + state = state, + onManageAccountClick = onManageAccountClick, + onLinkNewDeviceClick = onLinkNewDeviceClick, + onOpenBlockedUsers = onOpenBlockedUsers + ) + // General section GeneralSection( state = state, @@ -116,12 +118,12 @@ fun PreferencesRootView( onSignOutClick = onSignOutClick, onDeactivateClick = onDeactivateClick, ) - // Version + Footer( version = state.version, deviceId = state.deviceId, onClick = if (!state.showDeveloperSettings) { - { state.eventSink(PreferencesRootEvent.OnVersionInfoClick) } + { state.eventSink(PreferencesRootEvents.OnVersionInfoClick) } } else { null } @@ -140,15 +142,13 @@ private fun ColumnScope.MultiAccountSection( ) state.otherSessions.forEach { matrixUser -> MatrixUserRow( - modifier = Modifier - .clickable { - state.eventSink(PreferencesRootEvent.SwitchToSession(matrixUser.userId)) - } - .padding(top = 2.dp, bottom = 2.dp, end = 8.dp), + modifier = Modifier.clickable { + state.eventSink(PreferencesRootEvents.SwitchToSession(matrixUser.userId)) + }, matrixUser = matrixUser, avatarSize = AvatarSize.AccountItem, - verticalSpaceWidth = 16.dp, ) + HorizontalDivider() } ListItem( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())), @@ -198,14 +198,6 @@ private fun ColumnScope.ManageAccountSection( onLinkNewDeviceClick: () -> Unit, onOpenBlockedUsers: () -> Unit, ) { - state.accountManagementUrl?.let { url -> - ListItem( - headlineContent = { Text(stringResource(id = CommonStrings.action_manage_account_and_devices)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())), - trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())), - onClick = { onManageAccountClick(url) }, - ) - } if (state.showLinkNewDevice) { ListItem( headlineContent = { Text(stringResource(id = CommonStrings.common_link_new_device)) }, @@ -213,15 +205,33 @@ private fun ColumnScope.ManageAccountSection( onClick = onLinkNewDeviceClick, ) } + state.accountManagementUrl?.let { url -> + ListItem( + headlineContent = { Text(stringResource(id = CommonStrings.action_manage_account)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())), + trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())), + onClick = { onManageAccountClick(url) }, + ) + } + + state.devicesManagementUrl?.let { url -> + ListItem( + headlineContent = { Text(stringResource(id = CommonStrings.action_manage_devices)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Devices())), + trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())), + onClick = { onManageAccountClick(url) }, + ) + } + if (state.showBlockedUsersItem) { ListItem( headlineContent = { Text(stringResource(id = CommonStrings.common_blocked_users)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), onClick = onOpenBlockedUsers, - trailingContent = ListItemContent.Text(state.nbOfBlockedUsers.toString()), ) } - if (state.accountManagementUrl != null || state.showLinkNewDevice || state.showBlockedUsersItem) { + + if (state.accountManagementUrl != null || state.devicesManagementUrl != null || state.showBlockedUsersItem) { HorizontalDivider() } } @@ -238,18 +248,6 @@ private fun ColumnScope.GeneralSection( onSignOutClick: () -> Unit, onDeactivateClick: () -> Unit, ) { - ListItem( - headlineContent = { Text(stringResource(id = CommonStrings.common_advanced_settings)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Settings())), - onClick = onOpenAdvancedSettings, - ) - if (state.showLabsItem) { - ListItem( - headlineContent = { Text(stringResource(id = R.string.screen_labs_title)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Labs())), - onClick = onOpenLabs, - ) - } ListItem( headlineContent = { Text(stringResource(id = CommonStrings.common_about)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())), @@ -269,17 +267,30 @@ private fun ColumnScope.GeneralSection( onClick = onOpenAnalytics, ) } - HorizontalDivider() + ListItem( + headlineContent = { Text(stringResource(id = CommonStrings.common_advanced_settings)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Settings())), + onClick = onOpenAdvancedSettings, + ) + + if (state.showLabsItem) { + ListItem( + headlineContent = { Text(stringResource(id = R.string.screen_labs_title)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Labs())), + onClick = onOpenLabs, + ) + } + ListItem( headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Close())), + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.SignOut())), style = ListItemStyle.Destructive, onClick = onSignOutClick, ) if (state.canDeactivateAccount) { ListItem( - headlineContent = { Text(stringResource(id = CommonStrings.action_delete_account)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Delete())), + headlineContent = { Text(stringResource(id = CommonStrings.action_deactivate_account)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Warning())), style = ListItemStyle.Destructive, onClick = onDeactivateClick, ) @@ -308,8 +319,9 @@ private fun ColumnScope.Footer( Text( modifier = Modifier .align(Alignment.CenterHorizontally) + .padding(top = 16.dp) .clickable(enabled = onClick != null, onClick = onClick ?: {}) - .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 24.dp), + .padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 24.dp), textAlign = TextAlign.Center, text = text, style = ElementTheme.typography.fontBodySmRegular, @@ -328,23 +340,19 @@ private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) { @PreviewWithLargeHeight @Composable -internal fun PreferencesRootViewLightPreview(@PreviewParameter(PreferencesRootStateProvider::class) state: PreferencesRootState) = - ElementPreviewLight( - drawableFallbackForImages = CommonDrawables.sample_avatar, - ) { ContentToPreview(state) } +internal fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = + ElementPreviewLight { ContentToPreview(matrixUser) } @PreviewWithLargeHeight @Composable -internal fun PreferencesRootViewDarkPreview(@PreviewParameter(PreferencesRootStateProvider::class) state: PreferencesRootState) = - ElementPreviewDark( - drawableFallbackForImages = CommonDrawables.sample_avatar, - ) { ContentToPreview(state) } +internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = + ElementPreviewDark { ContentToPreview(matrixUser) } @ExcludeFromCoverage @Composable -private fun ContentToPreview(state: PreferencesRootState) { +private fun ContentToPreview(matrixUser: MatrixUser) { PreferencesRootView( - state = state, + state = aPreferencesRootState(myUser = matrixUser), onBackClick = {}, onAddAccountClick = {}, onOpenAnalytics = {}, @@ -364,3 +372,16 @@ private fun ContentToPreview(state: PreferencesRootState) { onDeactivateClick = {}, ) } + +@PreviewsDayNight +@Composable +internal fun MultiAccountSectionPreview() = ElementPreview { + Column { + MultiAccountSection( + state = aPreferencesRootState( + otherSessions = aMatrixUserList(), + ), + onAddAccountClick = {}, + ) + } +} 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 f3faf96ba2..6c26866e93 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,15 +33,12 @@ class DefaultClearCacheUseCase( private val matrixClient: MatrixClient, private val coroutineDispatchers: CoroutineDispatchers, private val defaultCacheService: DefaultCacheService, - private val okHttpClient: () -> OkHttpClient, + private val okHttpClient: Provider, 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 @@ -54,12 +51,7 @@ class DefaultClearCacheUseCase( // Clear OkHttp cache okHttpClient().cache?.delete() // Clear app cache - context.cacheDir?.listFiles { - // But keep the logs - it.name != "logs" - }?.onEach { - it.deleteRecursively() - } + context.cacheDir.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/UserPreferences.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/UserPreferences.kt index 43c7e8dacf..a9066dcd73 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/UserPreferences.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/UserPreferences.kt @@ -15,21 +15,21 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.MatrixUserHeader -import io.element.android.libraries.matrix.ui.components.MatrixUserProvider +import io.element.android.libraries.matrix.ui.components.MatrixUserWithNullProvider @Composable fun UserPreferences( - matrixUser: MatrixUser, + user: MatrixUser?, modifier: Modifier = Modifier, ) { MatrixUserHeader( modifier = modifier, - matrixUser = matrixUser, + matrixUser = user ) } @PreviewsDayNight @Composable -internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview { +internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserWithNullProvider::class) matrixUser: MatrixUser?) = ElementPreview { UserPreferences(matrixUser) } 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 2f238f9935..13f69a7e1e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt @@ -10,7 +10,6 @@ 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 @@ -29,7 +28,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 7f487afa24..b27a22bb8d 100644 --- a/features/preferences/impl/src/main/res/values-be/translations.xml +++ b/features/preferences/impl/src/main/res/values-be/translations.xml @@ -53,9 +53,6 @@ "налады сістэмы" "Сістэмныя апавяшчэнні выключаны" "Апавяшчэнні" - "Цёмная" - "Светлая" - "Сістэмная" "Выпраўленне непаладак" "Выпраўленне непаладак з апавяшчэннямі" 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 d5ad7facb2..692f0ac8f3 100644 --- a/features/preferences/impl/src/main/res/values-bg/translations.xml +++ b/features/preferences/impl/src/main/res/values-bg/translations.xml @@ -58,9 +58,6 @@ "системни настройки" "Системните известия са изключени" "Известия" - "Тъмен" - "Светъл" - "Система" "Отстраняване на неизправности" "Отстраняване на неизправности с известията" diff --git a/features/preferences/impl/src/main/res/values-ca/translations.xml b/features/preferences/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index b2cb548d45..0000000000 --- a/features/preferences/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - "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 5c690dd824..0127a98b41 100644 --- a/features/preferences/impl/src/main/res/values-cs/translations.xml +++ b/features/preferences/impl/src/main/res/values-cs/translations.xml @@ -11,15 +11,6 @@ "Skrýt avatary v žádostech o pozvání do místnosti" "Skrýt náhledy médií na časové ose" "Experimentální funkce" - "Vzdálenost, kterou musíte urazit, aby se spustila aktualizace." - "Ujistěte se, že je pro tuto aplikaci povolena možnost \"Přesná poloha\". Chcete-li změnit oprávnění, přejděte do %1$s." - "Nastavení aplikace" - "Aktuální informace o poloze" - - "Každý %1$d metr" - "Každé %1$d metry" - "Každých %1$d metrů" - "Rychlejší nahrávání fotografií a videí a snížení spotřeby dat" "Optimalizace kvality médií" "Moderování a bezpečnost" @@ -87,10 +78,6 @@ 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 e857f6aebe..9711eda179 100644 --- a/features/preferences/impl/src/main/res/values-cy/translations.xml +++ b/features/preferences/impl/src/main/res/values-cy/translations.xml @@ -70,9 +70,6 @@ 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 459ea33632..f258fb0952 100644 --- a/features/preferences/impl/src/main/res/values-da/translations.xml +++ b/features/preferences/impl/src/main/res/values-da/translations.xml @@ -11,14 +11,6 @@ "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" @@ -84,9 +76,6 @@ 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 e35cf871a1..c3722ec88c 100644 --- a/features/preferences/impl/src/main/res/values-de/translations.xml +++ b/features/preferences/impl/src/main/res/values-de/translations.xml @@ -76,9 +76,6 @@ 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 6db52343d3..64b651114d 100644 --- a/features/preferences/impl/src/main/res/values-el/translations.xml +++ b/features/preferences/impl/src/main/res/values-el/translations.xml @@ -76,9 +76,6 @@ "ρυθμίσεις συστήματος" "Ειδοποιήσεις συστήματος ανενεργές" "Ειδοποιήσεις" - "Σκοτεινό" - "Φωτεινό" - "Σύστημα" "Ιστορικό 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 dd496a1273..afdc23d90b 100644 --- a/features/preferences/impl/src/main/res/values-es/translations.xml +++ b/features/preferences/impl/src/main/res/values-es/translations.xml @@ -63,9 +63,6 @@ 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 d1e2b91d58..4e7ba1c26e 100644 --- a/features/preferences/impl/src/main/res/values-et/translations.xml +++ b/features/preferences/impl/src/main/res/values-et/translations.xml @@ -11,14 +11,6 @@ "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" @@ -84,10 +76,6 @@ Kui sa jätkad muutmist, siis võivad muutuda ka need peidetud eelistused.""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 42416c682b..42a90e0328 100644 --- a/features/preferences/impl/src/main/res/values-eu/translations.xml +++ b/features/preferences/impl/src/main/res/values-eu/translations.xml @@ -55,7 +55,4 @@ "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 df42ca953c..05e73febb4 100644 --- a/features/preferences/impl/src/main/res/values-fa/translations.xml +++ b/features/preferences/impl/src/main/res/values-fa/translations.xml @@ -19,15 +19,12 @@ "فراهم کنندهٔ آگاهی‌های ارسالی" "از کار انداختن ویرایشگر متن غنی یا نوشتن دستی مارک‌دون." "رسید‌های خواندن" - "اگر خاموش باشد، رسیدهای خوانده شدن شما برای کسی ارسال نمی‌شود. شما همچنان رسیدهای خوانده شدن را از سایر کاربران دریافت خواهید کرد." "هم‌رسانی حضور" - "اگر خاموش باشد، نمی‌توانید رسیدهای خوانده شدن یا اعلان‌های تایپ را ارسال یا دریافت کنید." "نهفتن همیشگی" "نمایش همیشگی" "در اتاق‌های خصوصی" "رسانه‌های نهفته همواره خواهند توانست با زدن رویشان نمایان شوند" "نمایش رسانه در خط زمانی" - "گزینه مشاهده منبع پیام در جدول زمانی را فعال کنید." "هیچ کاربر مسدودی ندارید" "رفع انسداد" "قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید." @@ -67,9 +64,6 @@ "تنظیمات سامانه" "آگاهی‌های سامانه‌ای خاموش شدند" "آگاهی‌ها" - "تیره" - "روشن" - "سامانه" "تاریخچهٔ آگاهی‌های ارسالی" "رفع‌اشکال" "رفع‌اشکال آگاهی‌ها" 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 99f900bcc1..f1462f3bb7 100644 --- a/features/preferences/impl/src/main/res/values-fi/translations.xml +++ b/features/preferences/impl/src/main/res/values-fi/translations.xml @@ -11,14 +11,6 @@ "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" @@ -80,21 +72,10 @@ Jos jatkat, jotkin asetukset saattavat muuttua." "Maininnat" "Ilmoita minulle" "Ilmoita minulle @room-maininnoista" - "Mukautettu ääni…" - "Virhe tiedoston poistamisessa" - "Elementin oletus" - "Virhe tiedoston tuonnissa" - "Ongelma ilmoitusäänen esikatselussa" - "Ääni" - "Ongelma ilmoitusäänen asettamisessa" "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 d7f17e4ffe..5ee91388bd 100644 --- a/features/preferences/impl/src/main/res/values-fr/translations.xml +++ b/features/preferences/impl/src/main/res/values-fr/translations.xml @@ -11,14 +11,6 @@ "Masquer les avatars des salons dans les invitations" "Masquer les aperçus des médias dans les discussions" "Expérimental" - "Distance à effectuer pour envoyer une mise à jour." - "Assurez-vous que la « Localisation précise » est activée pour cette application. Pour modifier l’autorisation, aller à: %1$s." - "Paramètres de l’application" - "Mises à jour en direct de la localisation" - - "%1$d mètre" - "%1$d mètres" - "Téléchargez des photos et des vidéos plus rapidement et réduisez la consommation de données" "Optimisez la qualité des médias" "Modération et sécurité" @@ -80,14 +72,10 @@ Si vous continuez, il est possible que certains de vos paramètres soient modifi "Mentions" "Prévenez-moi pour" "Prévenez-moi si un message contient \"@room\"" - "Son personnalisé…" "Pour recevoir des notifications, veuillez modifier votre %1$s." "paramètres du système" "Les notifications du système sont désactivées" "Notifications" - "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 d101146f8c..250af33d49 100644 --- a/features/preferences/impl/src/main/res/values-hr/translations.xml +++ b/features/preferences/impl/src/main/res/values-hr/translations.xml @@ -11,15 +11,6 @@ "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" @@ -85,9 +76,6 @@ 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 4c1b988696..c588e60eaf 100644 --- a/features/preferences/impl/src/main/res/values-hu/translations.xml +++ b/features/preferences/impl/src/main/res/values-hu/translations.xml @@ -11,14 +11,6 @@ "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" @@ -84,10 +76,6 @@ Ha folytatja, egyes beállítások megváltozhatnak." "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 9c853aed45..5ce794c8ac 100644 --- a/features/preferences/impl/src/main/res/values-in/translations.xml +++ b/features/preferences/impl/src/main/res/values-in/translations.xml @@ -72,9 +72,6 @@ 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 73555754fc..a10dcc9594 100644 --- a/features/preferences/impl/src/main/res/values-it/translations.xml +++ b/features/preferences/impl/src/main/res/values-it/translations.xml @@ -11,14 +11,6 @@ "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" @@ -84,9 +76,6 @@ 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 5cab6bdcf8..9cd0dfea96 100644 --- a/features/preferences/impl/src/main/res/values-ja/translations.xml +++ b/features/preferences/impl/src/main/res/values-ja/translations.xml @@ -11,16 +11,9 @@ "ルームへの招待リクエストにアバターを表示しない" "タイムラインにメディアのプレビューを表示しない" "ラボ" - "更新するのに必要な移動距離です。" - "「正確な位置情報」がこのアプリで使用可能なことを確認してください。権限を変更するには %1$s を開いてください。" - "アプリ設定" - "ライブ位置情報の更新" - - "%1$dm ごと" - - "写真や動画を高速で送信してデータ使用量を減らします。" + "写真や動画を高速で送信してデータ使用量を減らす" "メディアの品質を最適化" - "セキュリティと制限" + "制限と安全" "自動的に画像を最適化してアップロード時間とファイルサイズを削減します。" "画像のアップロード画質を最適化" "%1$s 変更するにはタップしてください。" @@ -53,7 +46,7 @@ "プロフィールを更新中…" "スレッドへの返信を有効化" "変更を適用するためにアプリケーションは再起動します。" - "開発段階の最新機能を試すことができます。未完成のため、変更や不安定な挙動が生じる可能性があります。" + "開発段階の最新機能を試します。未完成のため変更や不安定な挙動を生じる可能性があります。" "探究したいですか?" "ラボ" "追加設定" @@ -68,7 +61,7 @@ "すべてのメッセージ" "メンションとキーワードのみ" "ダイレクトチャットで以下の通知を受け取る" - "グループチャットで以下の通知を受け取る" + "グループチャットでは以下の通知を受け取る" "この端末で通知を受け取る" "設定が修正されていません。再試行してください。" "グループチャット" @@ -77,50 +70,12 @@ "メンション" "すべて" "メンション" - "以下を通知" + "以下を通知する" @ルームで通知を受け取る - "カスタム着信音" - "ファイルの削除に失敗" - "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 5a20e718a0..1efe197267 100644 --- a/features/preferences/impl/src/main/res/values-ka/translations.xml +++ b/features/preferences/impl/src/main/res/values-ka/translations.xml @@ -50,9 +50,6 @@ "სისტემის პარამეტრები" "სისტემის შეტყობინებები გამორთულია" "შეტყობინებები" - "მუქი" - "ღია" - "სისტემა" "პრობლემების გადაჭრა" "პრობლემების გადაჭრის შეტყობინებები" 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 e2c5e8e83b..5e17d57a87 100644 --- a/features/preferences/impl/src/main/res/values-ko/translations.xml +++ b/features/preferences/impl/src/main/res/values-ko/translations.xml @@ -77,9 +77,6 @@ "시스템 설정" "시스템 알림이 꺼져 있습니다." "알림" - "다크" - "라이트" - "시스템" "푸시 기록" "문제 해결" "문제 해결 알림" 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 ac67e04259..90ec12a1a1 100644 --- a/features/preferences/impl/src/main/res/values-nb/translations.xml +++ b/features/preferences/impl/src/main/res/values-nb/translations.xml @@ -76,9 +76,6 @@ 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 c5c439a0c3..28614965c5 100644 --- a/features/preferences/impl/src/main/res/values-nl/translations.xml +++ b/features/preferences/impl/src/main/res/values-nl/translations.xml @@ -54,9 +54,6 @@ 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 ca81984032..9e2c56e580 100644 --- a/features/preferences/impl/src/main/res/values-pl/translations.xml +++ b/features/preferences/impl/src/main/res/values-pl/translations.xml @@ -11,15 +11,6 @@ "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" @@ -65,7 +56,7 @@ Niektóre ustawienia mogą ulec zmianie, jeśli kontynuujesz." "Czaty prywatne" - "Własne ustawienia dla wybranego czatu" + "Ustawienia własne wybranego czatu" "Wystąpił błąd podczas aktualizacji ustawienia powiadomień." "Wszystkie wiadomości" "Tylko wzmianki i słowa kluczowe" @@ -81,48 +72,10 @@ Niektóre ustawienia mogą ulec zmianie, jeśli kontynuujesz." "Wzmianki" "Powiadamiaj mnie przez" "Powiadom mnie na @pokój" - "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" - "Nie udało się ustawić dźwięku powiadomień" - "Alert" - "Oczekiwanie" - "Dzwonek" - "Rozkwit" - "Kalipso" - "Gong" - "Ciuchcia" - "Opadający" - "Elektroniczna" - "Fanfary" - "Szkło" - "Róg" - "Drabina" - "Minuet" - "Wiadomości" - "Kryminał" - "Las Sherwood" - "Zaklęcie" - "Napięcie" - "Świst" - "Telegraf" - "Na palcach" - "Trójdźwięk" - "Tweet" - "Maszyna do pisania" - "Aktualizacja" "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 be354681f5..f6e1bc90ba 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,9 +76,6 @@ 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 0e27d58de5..ffd38b961f 100644 --- a/features/preferences/impl/src/main/res/values-pt/translations.xml +++ b/features/preferences/impl/src/main/res/values-pt/translations.xml @@ -76,9 +76,6 @@ 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 69eddf05f5..6f8e41c5b9 100644 --- a/features/preferences/impl/src/main/res/values-ro/translations.xml +++ b/features/preferences/impl/src/main/res/values-ro/translations.xml @@ -11,15 +11,6 @@ "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ță" @@ -87,10 +78,6 @@ 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 eb4ad2245d..77be6fd8db 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -11,15 +11,6 @@ "Скрывать аватары в приглашениях" "Скрывать предпросмотр медиа в истории сообщений" "Лаборатория" - "Расстояние, которое нужно пройти, чтобы инициировать обновление." - "Убедитесь, что для этого приложения включена функция «Точное местоположение». Чтобы изменить разрешение, перейдите на страницу %1$s." - "Настройки приложения" - "Обновления местоположения в режиме реального времени" - - "Каждый %1$d метр" - "Каждые %1$d метра" - "Каждые %1$d метров" - "Загружайте фотографии и видео быстрее и сокращайте потребление трафика" "Оптимизировать качество мультимедиа" "Модерация и безопасность" @@ -85,10 +76,6 @@ "системные настройки" "Системные уведомления выключены" "Уведомления" - "Черный" - "Темная" - "Светлое" - "Системное" "История уведомлений" "Устранение неполадок" "Уведомления об устранении неполадок" 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 530fccd90b..9968c6d4cb 100644 --- a/features/preferences/impl/src/main/res/values-sk/translations.xml +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -78,9 +78,6 @@ 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 109000440f..e25e36a44f 100644 --- a/features/preferences/impl/src/main/res/values-sv/translations.xml +++ b/features/preferences/impl/src/main/res/values-sv/translations.xml @@ -70,9 +70,6 @@ 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 78e9018554..0df8500fda 100644 --- a/features/preferences/impl/src/main/res/values-tr/translations.xml +++ b/features/preferences/impl/src/main/res/values-tr/translations.xml @@ -74,9 +74,6 @@ 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 4d8d738122..d145724af8 100644 --- a/features/preferences/impl/src/main/res/values-uk/translations.xml +++ b/features/preferences/impl/src/main/res/values-uk/translations.xml @@ -11,15 +11,6 @@ "Сховати аватари у запитах на запрошення до кімнат" "Сховати попередній перегляд медіа у стрічці" "Лабораторії" - "Відстань, яку потрібно пройти, щоб ініціювати оновлення." - "Переконайтеся, що для цього додатка увімкнено функцію «Точна геолокація». Щоб змінити дозвіл, перейдіть на сторінку %1$s." - "Налаштування додатка" - "Оновлення місцезнаходження в реальному часі" - - "Кожен %1$d метр" - "Кожні %1$d метри" - "Кожні %1$d метрів" - "Швидше завантажуйте фотографії та відео та зменшуйте використання даних" "Оптимізуйте медіаякість" "Модерування й безпека" @@ -85,9 +76,6 @@ "системні налаштування" "Системні сповіщення вимкнені" "Сповіщення" - "Темна" - "Світла" - "Системна" "Історія 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 35b8f4aa4e..470763e877 100644 --- a/features/preferences/impl/src/main/res/values-ur/translations.xml +++ b/features/preferences/impl/src/main/res/values-ur/translations.xml @@ -53,9 +53,6 @@ "نظام کی ترتیبات" "نظام کی اطلاعات بند کر دی گئیں" "اطلاعات" - "اندھیرا" - "روشنی" - "نظام" "ازالہ کریں" "اطلاعات کا ازالہ کریں" 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 884027f9b8..9c81fc3daa 100644 --- a/features/preferences/impl/src/main/res/values-uz/translations.xml +++ b/features/preferences/impl/src/main/res/values-uz/translations.xml @@ -76,9 +76,6 @@ 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 9889b1697a..db242130ed 100644 --- a/features/preferences/impl/src/main/res/values-vi/translations.xml +++ b/features/preferences/impl/src/main/res/values-vi/translations.xml @@ -1,7 +1,5 @@ - "Để đảm bảo bạn không bỏ lỡ bất kỳ cuộc gọi quan trọng nào, vui lòng thay đổi cài đặt để cho phép thông báo toàn màn hình khi điện thoại của bạn bị khóa." - "Nâng cao trải nghiệm cuộc gọi của bạn" "Chọn cách nhận thông báo" "Chế độ nhà phát triển" "Cho phép truy cập vào các tính năng và chức năng dành cho nhà phát triển." @@ -9,9 +7,6 @@ "Đặt địa chỉ máy chủ cuộc gọi tùy chỉnh cho Element." "Địa chỉ URL không hợp lệ. Hãy kiểm tra lại giao thức (http/https) và địa chỉ chính xác." "Labs" - "Tải ảnh và video nhanh hơn và giảm mức sử dụng dữ liệu." - "Tối ưu hóa chất lượng media" - "Nhà cung cấp dịch vụ thông báo" "Tắt trình soạn thảo văn bản nâng cao để nhập Markdown thủ công." "Thông báo đã đọc" "Nếu tắt, thông báo đã đọc của bạn sẽ không được gửi cho ai. Bạn vẫn sẽ nhận được thông báo đã đọc từ người khác." @@ -61,9 +56,6 @@ 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 03892ce980..634911100a 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,9 +76,6 @@ "系統設定" "已關閉系統通知" "通知" - "深色" - "淺色" - "系統" "推播通知歷史紀錄" "疑難排解" "疑難排解通知" 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 3726b82206..0dac7ab6d8 100644 --- a/features/preferences/impl/src/main/res/values-zh/translations.xml +++ b/features/preferences/impl/src/main/res/values-zh/translations.xml @@ -1,32 +1,25 @@ - "为确保你不会错过重要来电,请更改设置以允许锁屏时的全屏通知。" + "为确保您不会错过重要来电,请更改设置以允许锁屏时的全屏通知。" "提升通话体验" "选择如何接收通知" "开发者模式" - "启用以访问适用于开发者的功能与特性。" - "自定义 Element Call 基础 URL" - "为 Element Call 设置基础 URL。" + "允许开发人员访问特性和功能。" + "自定义 Element Call URL" + "为 Element 通话设置基本 URL。" "URL 无效,请确保包含协议(http/https)和正确的地址。" "在房间邀请请求中隐藏头像" - "在时间线上隐藏媒体预览" + "在时间轴中隐藏媒体预览" "实验室" - "触发一次更新所需的移动距离。" - "确保已为 app 启用“精确位置”。如需更改该权限请转到 %1$s。" - "App 设置" - "实时位置更新" - - "每 %1$d 米" - "针对上传进行优化" - "优化媒体质量" - "尺度与安全" + "媒体" + "内容审核与安全" "自动优化图像以实现更快的上传速度和更小的文件大小。" "优化图片上传质量" "%1$s。点击此处更改。" - "高(1080p)" - "低(480p)" - "标准(720p)" + "高 (1080p)" + "低画质 (480p)" + "标准 (720p)" "视频上传质量" "通知推送提供者" "禁用富文本编辑器,手动输入 Markdown。" @@ -37,91 +30,53 @@ "始终隐藏" "始终显示" "在私人房间" - "点击隐藏的媒体即可将其恢复显示" - "在时间线上显示媒体" - "启用在时间线上查看消息源码的选项。" - "暂无已屏蔽的用户" + "随时可以通过点击隐藏的媒体来显示它" + "在时间轴中显示媒体" + "启用在时间轴中查看消息源码的选项。" + "您没有屏蔽用户" "解除屏蔽" "可以重新接收他们的消息。" "解除屏蔽用户" - "正在解除屏蔽…" + "正在解除屏蔽……" "显示名称" - "你的显示名称" + "您的显示名称" "遇到未知错误,无法更改信息。" "无法更新个人资料" "编辑个人资料" - "正在更新个人资料…" - "启用消息列中的回复" - "App 将重启以应用此更改。" + "更新个人资料……" + "启用主题回复" + "应用将重启以应用此更改。" "尝试我们最新的开发理念。这些功能尚未最终确定,可能不稳定,也可能会发生变化。" "想尝试新功能?" "实验室" "更多设置" "音视频通话" "配置不匹配" - "我们简化了通知设置,使选项更易于查找。你曾经选择的某些自定义设置未在此处显示,但它们仍然有效。 + "我们简化了通知设置,使选项更易于查找。您过去选择的某些自定义设置未在此处显示,但它们仍然有效。 -如果继续,你的某些设置可能会被更改。" +如果继续,您的某些设置可能会更改。" "私聊" - "各房间单独的设置" + "各聊天室的独立设置" "更新通知设置时出错。" - "所有消息" + "全部消息" "仅限提及和关键词" - "在私聊中通知我以下类型" - "在群聊中通知我以下类型" + "在私聊中,请通知我:" + "在群聊中,请通知我:" "在此设备上启用通知" "配置尚未更正,请重试。" "群聊" "邀请" - "主服务器不支持在加密房间中的此选项,因此在某些房间你可能无法收到通知。" + "服务器在加密聊天室中不支持此选项,因此在某些聊天室可能无法收到通知。" "提及" "全部" "提及" - "通知我以下类型" - "提及所有成员(@room)时通知我" - "自定义声音…" - "删除文件时出错" - "Element 默认" - "Element 淡入" - "导入文件时出错" - "预览提示音时出现问题" - "声音" - "设置提示音时出现问题" - "提醒声" - "悉心期盼" - "铃铛" - "百花怒放" - "即兴曲调" - "风铃" - "火车鸣笛" - "渐弱" - "电子乐" - "嘹亮吹奏声" - "玻璃声" - "圆号" - "连音效果" - "小步舞曲" - "新闻快讯" - "夜色" - "绿林好汉的旋律" - "神奇魔咒" - "侦探悬念" - "嗖嗖声" - "敲打电报" - "蹑手蹑脚" - "三全音" - "鸟鸣声" - "敲打字机" - "整点新闻" - "要接收通知,请更改 %1$s。" + "请通知我:" + @room 时通知我 + "要接收通知,请更改您的 %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 7389a0433c..31ee676226 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -11,14 +11,6 @@ "Hide avatars in room invite requests" "Hide media previews in timeline" "Labs" - "The distance you have to move to trigger an update." - "Make sure \"Precise Location” is enabled for this app. To change the permission go to %1$s." - "App Settings" - "Live location updates" - - "Every %1$d meter" - "Every %1$d meters" - "Upload photos and videos faster and reduce data usage" "Optimise media quality" "Moderation and Safety" @@ -80,48 +72,10 @@ If you proceed, some of your settings may change." "Mentions" "Notify me for" "Notify me on @room" - "Custom sound…" - "Error deleting file" - "Element Default" - "Element Fade" - "Error importing file" - "Problem previewing alert sound" - "Sound" - "Problem setting alert sound" - "Alert" - "Anticipate" - "Bell" - "Bloom" - "Calypso" - "Chime" - "Choo Choo" - "Descent" - "Electronic" - "Fanfare" - "Glass" - "Horn" - "Ladder" - "Minuet" - "News Flash" - "Noir" - "Sherwood Forest" - "Spell" - "Suspense" - "Swish" - "Telegraph" - "Tiptoes" - "Tri-tone" - "Tweet" - "Typewriters" - "Update" "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 e7ce526843..258e9855de 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -22,47 +19,51 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AboutViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on back invokes back callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes back callback`() { ensureCalledOnce { callback -> - setAboutView( + rule.setAboutView( anAboutState(), onBackClick = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on an item invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on an item invokes the expected callback`() { val state = anAboutState() ensureCalledOnceWithParam(state.elementLegals.first()) { callback -> - setAboutView( + rule.setAboutView( state, onElementLegalClick = callback, ) - clickOn(state.elementLegals.first().titleRes) + rule.clickOn(state.elementLegals.first().titleRes) } } @Test - fun `clicking on the open source licenses invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on the open source licenses invokes the expected callback`() { ensureCalledOnce { callback -> - setAboutView( + rule.setAboutView( anAboutState(), onOpenSourceLicensesClick = callback, ) - clickOn(CommonStrings.common_open_source_licenses) + rule.clickOn(CommonStrings.common_open_source_licenses) } } } -private fun AndroidComposeUiTest.setAboutView( +private fun AndroidComposeTestRule.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 25d7c01778..942d549dab 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 @@ -12,17 +12,14 @@ 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.compound.theme.Theme 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 import io.element.android.tests.testutils.WarmUpRule -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -43,9 +40,6 @@ class AdvancedSettingsPresenterTest { assertThat(isSharePresenceEnabled).isTrue() assertThat(mediaOptimizationState).isNull() assertThat(theme).isEqualTo(ThemeOption.System) - assertThat(availableThemeOptions).isEqualTo( - listOf(ThemeOption.System, ThemeOption.Light, ThemeOption.Dark).toImmutableList() - ) assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse() assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) assertThat(mediaPreviewConfigState.setHideInviteAvatarsAction).isEqualTo(AsyncAction.Uninitialized) @@ -210,82 +204,6 @@ 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( - featureFlagService = FakeFeatureFlagService().apply { - setFeatureEnabled(FeatureFlags.AllowBlackTheme, true) - } - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - - with(awaitItem()) { - assertThat(availableThemeOptions).contains(ThemeOption.Black) - assertThat(availableThemeOptions).isEqualTo(ThemeOption.entries.toImmutableList()) - } - } - } - - @Test - fun `present - stored black theme falls back to dark when feature flag disabled`() = runTest { - val appPreferencesStore = InMemoryAppPreferencesStore().apply { - setTheme(Theme.Black.name) - } - val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - skipItems(1) - - with(awaitItem()) { - assertThat(theme).isEqualTo(ThemeOption.Dark) - } - } - } - @Test fun `present - hide invite avatars`() = runTest { val mediaPreviewStore = FakeMediaPreviewConfigStateStore() @@ -379,7 +297,7 @@ class AdvancedSettingsPresenterTest { } private fun CoroutineScope.createAdvancedSettingsPresenter( - appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + appPreferencesStore: InMemoryAppPreferencesStore = 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 302a209578..36fd30983e 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,16 +6,12 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Interaction @@ -28,104 +24,82 @@ import io.element.android.services.analytics.compose.LocalAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder -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 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() + @Test - fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on other theme emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on other theme emits the expected event`() { val eventsRecorder = EventsRecorder() - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.common_appearance) - clickOn(R.string.theme_dark) + rule.clickOn(CommonStrings.common_appearance) + rule.clickOn(CommonStrings.common_dark) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark)) } @Test - fun `black theme is shown when available`() = runAndroidComposeUiTest { - setAdvancedSettingsView( - state = aAdvancedSettingsState( - availableThemeOptions = ThemeOption.entries.toImmutableList(), - ), - ) - clickOn(CommonStrings.common_appearance) - run { - val text = activity!!.getString(R.string.theme_black) - onNodeWithText(text).assertExists() - } - } - - @Test - fun `black theme is hidden when unavailable`() = runAndroidComposeUiTest { - setAdvancedSettingsView( - state = aAdvancedSettingsState( - availableThemeOptions = ThemeOption.entries.filterNot { it == ThemeOption.Black }.toImmutableList(), - ), - ) - clickOn(CommonStrings.common_appearance) - assertNoNodeWithText(R.string.theme_black) - } - - @Test - fun `clicking on View source emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on View source emits the expected event`() { val eventsRecorder = EventsRecorder() - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_view_source) + rule.clickOn(CommonStrings.action_view_source) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) } @Test - fun `clicking on Share presence emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on Share presence emits the expected event`() { val eventsRecorder = EventsRecorder() - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_advanced_settings_share_presence) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on media to enable compression emits the expected event`() { val eventsRecorder = EventsRecorder() val analyticsService = FakeAnalyticsService() - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), analyticsService = analyticsService ) - clickOn(R.string.screen_advanced_settings_media_compression_description) + rule.clickOn(R.string.screen_advanced_settings_media_compression_description) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(true)) assertThat(analyticsService.capturedEvents).isEqualTo( listOf( @@ -137,17 +111,17 @@ class AdvancedSettingsViewTest { } @Test - fun `clicking on media to disable compression emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on media to disable compression emits the expected event`() { val eventsRecorder = EventsRecorder() val analyticsService = FakeAnalyticsService() - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( mediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = true), eventSink = eventsRecorder, ), analyticsService = analyticsService ) - clickOn(R.string.screen_advanced_settings_media_compression_description) + rule.clickOn(R.string.screen_advanced_settings_media_compression_description) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(false)) assertThat(analyticsService.capturedEvents).isEqualTo( listOf( @@ -160,65 +134,65 @@ class AdvancedSettingsViewTest { @Test @Config(qualifiers = "h1080dp") - fun `clicking on hide invite avatars emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on hide invite avatars emits the expected event`() { val eventsRecorder = EventsRecorder() - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, hideInviteAvatars = false ), ) - clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on timeline media preview always hide emits the expected event`() { val eventsRecorder = EventsRecorder() - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.On ), ) - clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on timeline media preview private rooms emits the expected event`() { val eventsRecorder = EventsRecorder() - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.On ), ) - clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on timeline media preview always show emits the expected event`() { val eventsRecorder = EventsRecorder() - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.Off ), ) - clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show) + rule.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`() = runAndroidComposeUiTest { + fun `hide invite avatars toggle is disabled when action is loading`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, hideInviteAvatars = false, @@ -226,14 +200,14 @@ class AdvancedSettingsViewTest { ), ) // The toggle should be disabled, so clicking should not emit any events - clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) + rule.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`() = runAndroidComposeUiTest { + fun `timeline media preview options are disabled when action is loading`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setAdvancedSettingsView( + rule.setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.On, @@ -241,16 +215,15 @@ class AdvancedSettingsViewTest { ), ) // The options should be disabled, so clicking should not emit any events - clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) - clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) + rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) + rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) } } -private fun AndroidComposeUiTest.setAdvancedSettingsView( +private fun AndroidComposeTestRule.setAdvancedSettingsView( state: AdvancedSettingsState, analyticsService: AnalyticsService = FakeAnalyticsService(), onBackClick: () -> Unit = EnsureNeverCalled(), - onOpenAppSettings: () -> Unit = EnsureNeverCalled(), ) { setContent { CompositionLocalProvider( @@ -259,7 +232,6 @@ private fun AndroidComposeUiTest.setAdvancedSettingsView( 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 993d14caab..b3549762ab 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -26,67 +23,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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BlockedUserViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `clicking on back invokes back callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes back callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setBlockedUsersView( + rule.setBlockedUsersView( aBlockedUsersState( eventSink = eventsRecorder ), onBackClick = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on a user emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on a user emits the expected Event`() { val eventsRecorder = EventsRecorder() val userList = aMatrixUserList() - setBlockedUsersView( + rule.setBlockedUsersView( aBlockedUsersState( blockedUsers = userList, eventSink = eventsRecorder ), ) - onNodeWithText(userList.first().displayName.orEmpty()).performClick() + rule.onNodeWithText(userList.first().displayName.orEmpty()).performClick() eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId)) } @Test - fun `clicking on cancel sends a BlockedUsersEvents`() = runAndroidComposeUiTest { + fun `clicking on cancel sends a BlockedUsersEvents`() { val eventsRecorder = EventsRecorder() - setBlockedUsersView( + rule.setBlockedUsersView( aBlockedUsersState( unblockUserAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(BlockedUsersEvents.Cancel) } @Test - fun `clicking on confirm sends a BlockedUsersEvents`() = runAndroidComposeUiTest { + fun `clicking on confirm sends a BlockedUsersEvents`() { val eventsRecorder = EventsRecorder() - setBlockedUsersView( + rule.setBlockedUsersView( aBlockedUsersState( unblockUserAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - clickOn(R.string.screen_blocked_users_unblock_alert_action) + rule.clickOn(R.string.screen_blocked_users_unblock_alert_action) eventsRecorder.assertSingle(BlockedUsersEvents.ConfirmUnblock) } } -private fun AndroidComposeUiTest.setBlockedUsersView( +private fun AndroidComposeTestRule.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 61d7278a8a..d4d02d7de9 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt @@ -6,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.tests.testutils.EnsureNeverCalled @@ -23,71 +20,76 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class DeveloperSettingsViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setDeveloperSettingsView( + rule.setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - pressBack() + rule.pressBack() } } @Config(qualifiers = "h2000dp") @Test - fun `clicking on push history notification invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on push history notification invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setDeveloperSettingsView( + rule.setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), onPushHistoryClick = it ) - clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title) + rule.clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title) } } @Config(qualifiers = "h2000dp") @Test - fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on open showkase invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setDeveloperSettingsView( + rule.setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), onOpenShowkase = it ) - onNodeWithText("Open Showkase browser").performClick() + rule.onNodeWithText("Open Showkase browser").performClick() } } @Config(qualifiers = "h2200dp") @Test - fun `clicking on clear cache emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on clear cache emits the expected event`() { val eventsRecorder = EventsRecorder() - setDeveloperSettingsView( + rule.setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), ) - onNodeWithText("Clear cache").performClick() + rule.onNodeWithText("Clear cache").performClick() eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache) } } -private fun AndroidComposeUiTest.setDeveloperSettingsView( +private fun AndroidComposeTestRule.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 17218c6ab5..123f31ae8e 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,22 +5,19 @@ * 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem @@ -30,73 +27,78 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class AppDeveloperSettingsPageTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setAppDeveloperSettingsView( + rule.setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - pressBack() + rule.pressBack() } } @Config(qualifiers = "h1500dp") @Test - fun `clicking on element call url open the dialogs and submit emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on element call url open the dialogs and submit emits the expected event`() { val eventsRecorder = EventsRecorder() - setAppDeveloperSettingsView( + rule.setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), ) - clickOn(R.string.screen_advanced_settings_element_call_base_url) - val textInputNode = onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) + rule.clickOn(R.string.screen_advanced_settings_element_call_base_url) + val textInputNode = rule.onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) textInputNode.performTextInput("https://call.element.dev") - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl("https://call.element.dev")) } @Config(qualifiers = "h2000dp") @Test - fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on open showkase invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setAppDeveloperSettingsView( + rule.setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), onOpenShowkase = it ) - onNodeWithText("Open Showkase browser").performClick() + rule.onNodeWithText("Open Showkase browser").performClick() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on log level emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on log level emits the expected event`() { val eventsRecorder = EventsRecorder() - setAppDeveloperSettingsView( + rule.setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), ) - onNodeWithText("Tracing log level").performClick() - onNodeWithText("Debug").performClick() + rule.onNodeWithText("Tracing log level").performClick() + rule.onNodeWithText("Debug").performClick() eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetTracingLogLevel(LogLevelItem.DEBUG)) } } -private fun AndroidComposeUiTest.setAppDeveloperSettingsView( +private fun AndroidComposeTestRule.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 3d2cc85e54..0e9d774e84 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,9 +13,13 @@ 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 @@ -64,6 +68,18 @@ 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() @@ -140,11 +156,13 @@ 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 509d00db25..e03b65f0a9 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, - isDm = false, + isOneToOne = 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 26bb0ba1ee..9b36c477a4 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 @@ -61,8 +61,8 @@ class NotificationSettingsPresenterTest { val notificationSettingsService = FakeNotificationSettingsService() val presenter = createNotificationSettingsPresenter(notificationSettingsService) presenter.test { - notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, isDM = false, mode = RoomNotificationMode.ALL_MESSAGES) - notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, isDM = false, mode = RoomNotificationMode.ALL_MESSAGES) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES) val updatedState = consumeItemsUntilPredicate { (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid) ?.defaultGroupNotificationMode == RoomNotificationMode.ALL_MESSAGES @@ -79,12 +79,12 @@ class NotificationSettingsPresenterTest { presenter.test { notificationSettingsService.setDefaultRoomNotificationMode( isEncrypted = true, - isDM = false, + isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES ) notificationSettingsService.setDefaultRoomNotificationMode( isEncrypted = false, - isDM = false, + isOneToOne = false, mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY ) val updatedState = consumeItemsUntilPredicate { 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 66ed0339a3..ea140abbd7 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -28,71 +25,76 @@ 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 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() + @Test - fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes the expected callback`() { val eventsRecorder = EventsRecorder() ensureCalledOnce { - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - pressBack() + rule.pressBack() } eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on troubleshoot notification invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on troubleshoot notification invokes the expected callback`() { val eventsRecorder = EventsRecorder() ensureCalledOnce { - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onTroubleshootNotificationsClick = it ) - clickOn(R.string.troubleshoot_notifications_entry_point_title) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on group chats invokes the expected callback`() { val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(false) { - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onOpenEditDefault = it ) - clickOn(R.string.screen_notification_settings_group_chats) + rule.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`() = runAndroidComposeUiTest { + fun `clicking on direct chats invokes the expected callback`() { val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(true) { - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onOpenEditDefault = it ) - clickOn(R.string.screen_notification_settings_direct_chats) + rule.clickOn(R.string.screen_notification_settings_direct_chats) } eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } @@ -109,15 +111,15 @@ class NotificationSettingsViewTest { testNotificationToggle(false) } - private fun testNotificationToggle(initialState: Boolean) = runAndroidComposeUiTest { + private fun testNotificationToggle(initialState: Boolean) { val eventsRecorder = EventsRecorder() - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aValidNotificationSettingsState( appNotificationEnabled = initialState, eventSink = eventsRecorder ), ) - clickOn(R.string.screen_notification_settings_enable_notifications) + rule.clickOn(R.string.screen_notification_settings_enable_notifications) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -138,15 +140,15 @@ class NotificationSettingsViewTest { testAtRoomToggle(false) } - private fun testAtRoomToggle(initialState: Boolean) = runAndroidComposeUiTest { + private fun testAtRoomToggle(initialState: Boolean) { val eventsRecorder = EventsRecorder() - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aValidNotificationSettingsState( atRoomNotificationsEnabled = initialState, eventSink = eventsRecorder ), ) - clickOn(R.string.screen_notification_settings_room_mention_label) + rule.clickOn(R.string.screen_notification_settings_room_mention_label) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -167,15 +169,15 @@ class NotificationSettingsViewTest { testInvitationToggle(false) } - private fun testInvitationToggle(initialState: Boolean) = runAndroidComposeUiTest { + private fun testInvitationToggle(initialState: Boolean) { val eventsRecorder = EventsRecorder() - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aValidNotificationSettingsState( inviteForMeNotificationsEnabled = initialState, eventSink = eventsRecorder ), ) - clickOn(R.string.screen_notification_settings_invite_for_me_label) + rule.clickOn(R.string.screen_notification_settings_invite_for_me_label) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -186,15 +188,15 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `with an error configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest { + fun `with an error configuration, clicking on continue emits the expected events`() { val eventsRecorder = EventsRecorder() - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aValidNotificationSettingsState( changeNotificationSettingAction = AsyncAction.Failure(AN_EXCEPTION), eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -205,15 +207,15 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `with invalid configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest { + fun `with invalid configuration, clicking on continue emits the expected events`() { val eventsRecorder = EventsRecorder() - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aInvalidNotificationSettingsState( fixFailed = false, eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -224,15 +226,15 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `with invalid configuration and error, clicking on OK emits the expected events`() = runAndroidComposeUiTest { + fun `with invalid configuration and error, clicking on OK emits the expected events`() { val eventsRecorder = EventsRecorder() - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aInvalidNotificationSettingsState( fixFailed = true, eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -243,14 +245,14 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on Push notification provider emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on Push notification provider emits the expected event`() { val eventsRecorder = EventsRecorder() - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), ) - clickOn(R.string.screen_advanced_settings_push_provider_android) + rule.clickOn(R.string.screen_advanced_settings_push_provider_android) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -260,16 +262,16 @@ class NotificationSettingsViewTest { } @Test - fun `clicking on a push provider emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on a push provider emits the expected event`() { val eventsRecorder = EventsRecorder() - setNotificationSettingsView( + rule.setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder, showChangePushProviderDialog = true, availablePushDistributors = listOf(aDistributor("P1"), aDistributor("P2")) ), ) - onNodeWithText("P2").performClick() + rule.onNodeWithText("P2").performClick() eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -279,7 +281,7 @@ class NotificationSettingsViewTest { } } -private fun AndroidComposeUiTest.setNotificationSettingsView( +private fun AndroidComposeTestRule.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/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index a6861be12d..f0dc58ef22 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,8 +12,6 @@ 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 @@ -25,13 +23,11 @@ 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.oauth.AccountManagementAction +import io.element.android.libraries.matrix.api.oidc.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 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.A_USER_ID_2 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.core.aBuildMeta @@ -44,9 +40,7 @@ import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -67,9 +61,6 @@ class PreferencesRootPresenterTest { ) createPresenter( matrixClient = matrixClient, - sessionEnterpriseService = FakeSessionEnterpriseService( - tweakMasUrlResult = { "tweaked $it" }, - ), ).test { val initialState = awaitItem() assertThat(initialState.myUser).isEqualTo( @@ -82,7 +73,6 @@ class PreferencesRootPresenterTest { assertThat(initialState.version).isEqualTo("A Version") assertThat(initialState.isMultiAccountEnabled).isFalse() assertThat(initialState.otherSessions).isEmpty() - assertThat(initialState.version).isEqualTo("A Version") val loadedState = awaitItem() assertThat(loadedState.myUser).isEqualTo( MatrixUser( @@ -91,21 +81,27 @@ class PreferencesRootPresenterTest { avatarUrl = AN_AVATAR_URL ) ) + assertThat(initialState.version).isEqualTo("A Version") assertThat(loadedState.showSecureBackup).isFalse() assertThat(loadedState.showSecureBackupBadge).isFalse() assertThat(loadedState.accountManagementUrl).isNull() + assertThat(loadedState.devicesManagementUrl).isNull() assertThat(loadedState.showAnalyticsSettings).isFalse() assertThat(loadedState.showLinkNewDevice).isFalse() assertThat(loadedState.showDeveloperSettings).isTrue() assertThat(loadedState.canDeactivateAccount).isTrue() assertThat(loadedState.canReportBug).isTrue() - assertThat(loadedState.nbOfBlockedUsers).isEqualTo(0) assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState()) assertThat(loadedState.snackbarMessage).isNull() + skipItems(1) val finalState = awaitItem() - accountManagementUrlResult.assertions().isCalledOnce() - .with(value(null)) - assertThat(finalState.accountManagementUrl).isEqualTo("tweaked null url") + accountManagementUrlResult.assertions().isCalledExactly(2) + .withSequence( + listOf(value(AccountManagementAction.Profile)), + listOf(value(AccountManagementAction.DevicesList)), + ) + assertThat(finalState.accountManagementUrl).isEqualTo("Profile url") + assertThat(finalState.devicesManagementUrl).isEqualTo("DevicesList url") } } @@ -125,22 +121,6 @@ class PreferencesRootPresenterTest { } } - @Test - fun `present - number of blocked users`() = runTest { - val matrixClient = FakeMatrixClient( - canDeactivateAccountResult = { true }, - accountManagementUrlResult = { Result.success("") }, - ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2)), - ) - createPresenter( - matrixClient = matrixClient, - ).test { - skipItems(1) - val initialState = awaitItem() - assertThat(initialState.nbOfBlockedUsers).isEqualTo(2) - } - } - @Test fun `present - secure backup badge`() = runTest { val matrixClient = FakeMatrixClient( @@ -201,36 +181,12 @@ class PreferencesRootPresenterTest { val loadedState = awaitFirstItem() repeat(times = ShowDeveloperSettingsProvider.DEVELOPER_SETTINGS_COUNTER) { assertThat(loadedState.showDeveloperSettings).isFalse() - loadedState.eventSink(PreferencesRootEvent.OnVersionInfoClick) + loadedState.eventSink(PreferencesRootEvents.OnVersionInfoClick) } assertThat(awaitItem().showDeveloperSettings).isTrue() } } - @Test - fun `present - switch session invoke method on the session store`() = runTest { - val setLatestSessionResult = lambdaRecorder { } - val sessionStore = InMemorySessionStore( - initialList = listOf( - aSessionData(sessionId = A_SESSION_ID.value), - aSessionData(sessionId = A_SESSION_ID_2.value), - ), - setLatestSessionResult = setLatestSessionResult, - ) - createPresenter( - matrixClient = FakeMatrixClient( - canDeactivateAccountResult = { true }, - accountManagementUrlResult = { Result.success(null) }, - ), - sessionStore = sessionStore, - ).test { - val loadedState = awaitFirstItem() - loadedState.eventSink(PreferencesRootEvent.SwitchToSession(A_SESSION_ID_2)) - setLatestSessionResult.assertions().isCalledOnce() - .with(value(A_SESSION_ID_2.value)) - } - } - @Test fun `present - labs can be shown if any feature flag is in labs and not finished`() = runTest { createPresenter( @@ -332,7 +288,6 @@ class PreferencesRootPresenterTest { indicatorService: IndicatorService = FakeIndicatorService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), sessionStore: SessionStore = InMemorySessionStore(), - sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(), ) = PreferencesRootPresenter( matrixClient = matrixClient, sessionVerificationService = sessionVerificationService, @@ -345,6 +300,5 @@ 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 deleted file mode 100644 index 88ebbf64a1..0000000000 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt +++ /dev/null @@ -1,482 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -@file:OptIn(ExperimentalTestApi::class) - -package io.element.android.features.preferences.impl.root - -import androidx.activity.ComponentActivity -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.compose.ui.test.v2.runAndroidComposeUiTest -import androidx.test.ext.junit.runners.AndroidJUnit4 -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 -import io.element.android.libraries.matrix.ui.components.aMatrixUser -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.pressBack -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class PreferencesRootViewTest { - @Test - fun `clicking on back invokes back callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - eventSink = eventsRecorder - ), - onBackClick = callback, - ) - pressBack() - } - } - - @Test - fun `click on User profile invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - val user = aMatrixUser() - ensureCalledOnceWithParam(user) { callback -> - setView( - aPreferencesRootState( - myUser = user, - eventSink = eventsRecorder, - ), - onOpenUserProfile = callback, - ) - onNodeWithText("Alice").performClick() - } - } - - @Test - fun `clicking on other session sends a SwitchToSession`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() - setView( - aPreferencesRootState( - isMultiAccountEnabled = true, - otherSessions = listOf( - aMatrixUser( - id = A_USER_ID_2.value, - displayName = "Bob", - ) - ), - eventSink = eventsRecorder, - ), - ) - onNodeWithText("Bob").performClick() - eventsRecorder.assertSingle(PreferencesRootEvent.SwitchToSession(A_USER_ID_2)) - } - - @Test - fun `click on Add account invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - isMultiAccountEnabled = true, - eventSink = eventsRecorder, - ), - onAddAccountClick = callback, - ) - clickOn(CommonStrings.common_add_another_account) - } - } - - @Test - fun `when multi account is not enabled, item is not shown`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - setView( - aPreferencesRootState( - isMultiAccountEnabled = false, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(activity!!.getString(CommonStrings.common_add_another_account)).assertDoesNotExist() - } - - @Test - fun `click on Encryption invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - showSecureBackup = true, - eventSink = eventsRecorder, - ), - onSecureBackupClick = callback, - ) - clickOn(CommonStrings.common_encryption) - } - } - - @Test - fun `when showSecureBackup is false, item is not shown`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - setView( - aPreferencesRootState( - showSecureBackup = false, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(activity!!.getString(CommonStrings.common_encryption)).assertDoesNotExist() - } - - @Test - fun `click on Manage account invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnceWithParam("aUrl") { callback -> - setView( - aPreferencesRootState( - accountManagementUrl = "aUrl", - eventSink = eventsRecorder, - ), - onManageAccountClick = callback, - ) - clickOn(CommonStrings.action_manage_account_and_devices) - } - } - - @Test - fun `when accountManagementUrl is null, item is not shown`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - setView( - aPreferencesRootState( - accountManagementUrl = null, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(activity!!.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist() - } - - @Test - fun `click on Link new devices invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - showLinkNewDevice = true, - eventSink = eventsRecorder, - ), - onLinkNewDeviceClick = callback, - ) - clickOn(CommonStrings.common_link_new_device) - } - } - - @Test - fun `when showLinkNewDevice is false, item is not shown`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - setView( - aPreferencesRootState( - showLinkNewDevice = false, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(activity!!.getString(CommonStrings.common_link_new_device)).assertDoesNotExist() - } - - @Test - fun `click on Analytics invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - showAnalyticsSettings = true, - eventSink = eventsRecorder, - ), - onOpenAnalytics = callback, - ) - clickOn(CommonStrings.common_analytics) - } - } - - @Test - fun `when showAnalyticsSettings is false, item is not shown`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - setView( - aPreferencesRootState( - showAnalyticsSettings = false, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(activity!!.getString(CommonStrings.common_analytics)).assertDoesNotExist() - } - - @Test - fun `click on Report a problem invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - canReportBug = true, - eventSink = eventsRecorder, - ), - onOpenRageShake = callback, - ) - clickOn(CommonStrings.common_report_a_problem) - } - } - - @Test - fun `when canReportBug is false, item is not shown`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - setView( - aPreferencesRootState( - canReportBug = false, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(activity!!.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist() - } - - @Test - fun `click on Screen lock invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - eventSink = eventsRecorder, - ), - onOpenLockScreenSettings = callback, - ) - clickOn(CommonStrings.common_screen_lock) - } - } - - @Test - fun `click on About invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - eventSink = eventsRecorder, - ), - onOpenAbout = callback, - ) - clickOn(CommonStrings.common_about) - } - } - - @Test - fun `click on Developer settings invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - showDeveloperSettings = true, - eventSink = eventsRecorder, - ), - onOpenDeveloperSettings = callback, - ) - clickOn(CommonStrings.common_developer_options) - } - } - - @Test - fun `when showDeveloperSettings is false, item is not shown`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - setView( - aPreferencesRootState( - showDeveloperSettings = false, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(activity!!.getString(CommonStrings.common_developer_options)).assertDoesNotExist() - } - - @Test - fun `click on Advanced settings invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - eventSink = eventsRecorder, - ), - onOpenAdvancedSettings = callback, - ) - clickOn(CommonStrings.common_advanced_settings) - } - } - - @Test - fun `click on Labs invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - showLabsItem = true, - eventSink = eventsRecorder, - ), - onOpenLabs = callback, - ) - clickOn(R.string.screen_labs_title) - } - } - - @Test - fun `when showLabsItem is false, item is not shown`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - setView( - aPreferencesRootState( - showLabsItem = false, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(activity!!.getString(R.string.screen_labs_title)).assertDoesNotExist() - } - - @Test - fun `click on Notification invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - eventSink = eventsRecorder, - ), - onOpenNotificationSettings = callback, - ) - clickOn(R.string.screen_notification_settings_title) - } - } - - @Test - fun `click on Blocked users invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - nbOfBlockedUsers = 1, - eventSink = eventsRecorder, - ), - onOpenBlockedUsers = callback, - ) - clickOn(CommonStrings.common_blocked_users) - } - } - - @Test - fun `when nbOfBlockedUsers is 0, item is not shown`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - setView( - aPreferencesRootState( - nbOfBlockedUsers = 0, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(activity!!.getString(CommonStrings.common_blocked_users)).assertDoesNotExist() - } - - @Test - fun `click on Remove this device invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - eventSink = eventsRecorder, - ), - onSignOutClick = callback, - ) - clickOn(CommonStrings.action_signout) - } - } - - @Test - fun `click on Deactivate invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - setView( - aPreferencesRootState( - canDeactivateAccount = true, - eventSink = eventsRecorder, - ), - onDeactivateClick = callback, - ) - clickOn(CommonStrings.action_delete_account) - } - } - - @Test - fun `when canDeactivateAccount is false, item is not shown`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) - setView( - aPreferencesRootState( - canDeactivateAccount = false, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(activity!!.getString(CommonStrings.action_delete_account)).assertDoesNotExist() - } - - @Test - fun `clicking on version sends a PreferencesRootEvents`() = runAndroidComposeUiTest { - val version = "VERSION" - val eventsRecorder = EventsRecorder() - setView( - aPreferencesRootState( - version = version, - eventSink = eventsRecorder, - ), - ) - onNodeWithText(version).performClick() - eventsRecorder.assertSingle(PreferencesRootEvent.OnVersionInfoClick) - } -} - -private fun AndroidComposeUiTest.setView( - state: PreferencesRootState, - onBackClick: () -> Unit = EnsureNeverCalled(), - onAddAccountClick: () -> Unit = EnsureNeverCalled(), - onSecureBackupClick: () -> Unit = EnsureNeverCalled(), - onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(), - onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(), - onOpenAnalytics: () -> Unit = EnsureNeverCalled(), - onOpenRageShake: () -> Unit = EnsureNeverCalled(), - onOpenLockScreenSettings: () -> Unit = EnsureNeverCalled(), - onOpenAbout: () -> Unit = EnsureNeverCalled(), - onOpenDeveloperSettings: () -> Unit = EnsureNeverCalled(), - onOpenAdvancedSettings: () -> Unit = EnsureNeverCalled(), - onOpenLabs: () -> Unit = EnsureNeverCalled(), - onOpenNotificationSettings: () -> Unit = EnsureNeverCalled(), - onOpenUserProfile: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(), - onOpenBlockedUsers: () -> Unit = EnsureNeverCalled(), - onSignOutClick: () -> Unit = EnsureNeverCalled(), - onDeactivateClick: () -> Unit = EnsureNeverCalled(), -) { - setContent { - PreferencesRootView( - state = state, - onBackClick = onBackClick, - onAddAccountClick = onAddAccountClick, - onSecureBackupClick = onSecureBackupClick, - onManageAccountClick = onManageAccountClick, - onLinkNewDeviceClick = onLinkNewDeviceClick, - onOpenAnalytics = onOpenAnalytics, - onOpenRageShake = onOpenRageShake, - onOpenLockScreenSettings = onOpenLockScreenSettings, - onOpenAbout = onOpenAbout, - onOpenDeveloperSettings = onOpenDeveloperSettings, - onOpenAdvancedSettings = onOpenAdvancedSettings, - onOpenLabs = onOpenLabs, - onOpenNotificationSettings = onOpenNotificationSettings, - onOpenUserProfile = onOpenUserProfile, - onOpenBlockedUsers = onOpenBlockedUsers, - onSignOutClick = onSignOutClick, - onDeactivateClick = onDeactivateClick, - ) - } -} 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 1c1cb83def..6845ecb3a4 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,8 +19,6 @@ 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 @@ -51,9 +49,6 @@ 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, @@ -63,11 +58,9 @@ 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 20db955955..728e05ee7e 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,17 +6,14 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.media.AvatarAction @@ -26,93 +23,96 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EditUserProfileViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on back emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on back emits the expected event`() { val eventsRecorder = EventsRecorder() - setEditUserProfileView( + rule.setEditUserProfileView( aEditUserProfileState( eventSink = eventsRecorder, ), ) - pressBack() + rule.pressBack() eventsRecorder.assertSingle(EditUserProfileEvent.Exit) } @Test - fun `clicking on save from the exit confirmation dialog emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on save from the exit confirmation dialog emits the expected event`() { val eventsRecorder = EventsRecorder() - setEditUserProfileView( + rule.setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_save, inDialog = true) + rule.clickOn(CommonStrings.action_save, inDialog = true) eventsRecorder.assertSingle(EditUserProfileEvent.Save) } @Test - fun `clicking on discard exit emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on discard exit emits the expected event`() { val eventsRecorder = EventsRecorder() - setEditUserProfileView( + rule.setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_discard) + rule.clickOn(CommonStrings.action_discard) eventsRecorder.assertSingle(EditUserProfileEvent.Exit) } @Test - fun `clicking on save emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on save emits the expected event`() { val eventsRecorder = EventsRecorder() - setEditUserProfileView( + rule.setEditUserProfileView( aEditUserProfileState( saveButtonEnabled = true, saveAction = AsyncAction.Uninitialized, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_save) + rule.clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(EditUserProfileEvent.Save) } @Test - fun `clicking on avatar opens the bottom sheet dialog`() = runAndroidComposeUiTest { + fun `clicking on avatar opens the bottom sheet dialog`() { val eventsRecorder = EventsRecorder() val actions = listOf( AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, AvatarAction.Remove, ) - setEditUserProfileView( + rule.setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.Uninitialized, avatarActions = actions, eventSink = eventsRecorder, ), ) - val resources = activity!!.resources - val contentDescription = resources.getString(CommonStrings.a11y_avatar) - onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = rule.activity.getString(CommonStrings.a11y_avatar) + rule.onNodeWithContentDescription(contentDescription).performClick() // Assert that the actions are displayed actions.forEach { action -> - val text = resources.getString(action.titleResId) - onNodeWithText(text).assertExists() + val text = rule.activity.getString(action.titleResId) + rule.onNodeWithText(text).assertExists() } } @Test - fun `success invokes the expected callback`() = runAndroidComposeUiTest { + fun `success invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setEditUserProfileView( + rule.setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.Success(Unit), eventSink = eventsRecorder, @@ -123,7 +123,7 @@ class EditUserProfileViewTest { } } -private fun AndroidComposeUiTest.setEditUserProfileView( +private fun AndroidComposeTestRule.setEditUserProfileView( state: EditUserProfileState, onEditProfileSuccess: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/preferences/test/build.gradle.kts b/features/preferences/test/build.gradle.kts index a066fe4707..7e3da4a6e8 100644 --- a/features/preferences/test/build.gradle.kts +++ b/features/preferences/test/build.gradle.kts @@ -14,7 +14,6 @@ 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 ea78ed5a2c..5db1d4f076 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.common_report_a_problem), + title = stringResource(id = CommonStrings.action_report_bug), 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 c60bd4e1bf..745a362637 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.common_report_a_problem), + title = stringResource(id = CommonStrings.action_report_bug), 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/res/values-ca/translations.xml b/features/rageshake/api/src/main/res/values-ca/translations.xml deleted file mode 100644 index b5907369e6..0000000000 --- a/features/rageshake/api/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - "%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-zh/translations.xml b/features/rageshake/api/src/main/res/values-zh/translations.xml index 8c78bbbc9c..34a643ceab 100644 --- a/features/rageshake/api/src/main/res/values-zh/translations.xml +++ b/features/rageshake/api/src/main/res/values-zh/translations.xml @@ -1,7 +1,7 @@ - "%1$s 上次使用时曾崩溃过。是否与我们分享崩溃报告?" - "你似乎愤怒地摇晃了手机。是否打开 Bug 报告页面?" + "%1$s 上次使用时崩溃了。想和我们分享崩溃报告吗?" + "你似乎愤怒地摇晃了手机。想要打开 Bug 报告页面吗?" "摇一摇" "检测阈值" 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 2568a6fba9..b4e7faa01e 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,6 +14,7 @@ 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 @@ -76,7 +77,7 @@ class DefaultBugReporter( private val screenshotHolder: ScreenshotHolder, private val crashDataStore: CrashDataStore, private val coroutineDispatchers: CoroutineDispatchers, - private val okHttpClient: () -> OkHttpClient, + private val okHttpClient: Provider, private val userAgentProvider: UserAgentProvider, private val sessionStore: SessionStore, private val buildMeta: BuildMeta, diff --git a/features/rageshake/impl/src/main/res/values-ca/translations.xml b/features/rageshake/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 6586cd9dc2..0000000000 --- a/features/rageshake/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - "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-fa/translations.xml b/features/rageshake/impl/src/main/res/values-fa/translations.xml index 2e27f97ba6..ba5ab756db 100644 --- a/features/rageshake/impl/src/main/res/values-fa/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fa/translations.xml @@ -7,7 +7,6 @@ "لطفاً مشکل را شرح دهید. چه‌کار کردید؟ انتظار داشتید چه بشود؟ ولی چه شد؟ لطفاً‌تا جای ممکن وارد جزییات شوید." "شرح مشکل…" "ترجیحاً توضیحات را به زبان انگلیسی بنویسید." - "توضیحات خیلی کوتاه است، لطفاً جزئیات بیشتری در مورد آنچه اتفاق افتاده ارائه دهید. متشکرم!" "ارسال رخدادنگارهای خطا" "اجازه به گزارش‌ها" "ارسال تصویر صفحه" 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 c29306b9bf..3999133537 100644 --- a/features/rageshake/impl/src/main/res/values-uk/translations.xml +++ b/features/rageshake/impl/src/main/res/values-uk/translations.xml @@ -14,7 +14,5 @@ "Надіслати знімок екрана" "Журнали будуть додані до вашого повідомлення, щоб переконатися, що все працює належним чином. Щоб надіслати повідомлення без журналів, вимкніть це налаштування." "Стався збій %1$s під час останнього користування. Хочете поділитися з нами звітом про збій?" - "Якщо у вас виникають проблеми зі сповіщеннями, надсилання нам правил push-сповіщень допоможе нам визначити першопричину. Зверніть увагу, що ці правила можуть містити приватну інформацію, таку як ваше ім’я користувача або ключові слова, за якими ви отримували сповіщення." - "Налаштування сповіщень" "Переглянути журнали" diff --git a/features/rageshake/impl/src/main/res/values-vi/translations.xml b/features/rageshake/impl/src/main/res/values-vi/translations.xml index 6f53dc65e6..5a60edd206 100644 --- a/features/rageshake/impl/src/main/res/values-vi/translations.xml +++ b/features/rageshake/impl/src/main/res/values-vi/translations.xml @@ -10,11 +10,8 @@ "Phần mô tả quá ngắn, vui lòng cung cấp thêm chi tiết về những gì đã xảy ra. Cảm ơn!" "Gửi nhật ký sự cố" "Cho phép ghi nhật ký" - "Tệp nhật ký của bạn quá lớn nên không thể đưa vào báo cáo này, vui lòng gửi chúng cho chúng tôi bằng cách khác." "Gửi ảnh chụp màn hình" "Nhật ký lỗi sẽ được đính kèm với tin nhắn của bạn để đảm bảo mọi thứ hoạt động bình thường. Để gửi tin nhắn mà không có nhật ký lỗi, hãy tắt cài đặt này." "%1$s đã bị lỗi ở lần sử dụng gần nhất. Bạn có muốn chia sẻ báo cáo lỗi với chúng tôi không?" - "Nếu bạn gặp sự cố với thông báo, việc tải lên các quy tắc thông báo có thể giúp chúng tôi xác định nguyên nhân chính. Xin lưu ý rằng các quy tắc này có thể chứa thông tin riêng tư, chẳng hạn như tên hiển thị hoặc từ khóa mà bạn muốn nhận thông báo." - "Cài đặt thông báo" "Xem nhật ký" 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 c42141d291..527a35cdcc 100644 --- a/features/rageshake/impl/src/main/res/values-zh/translations.xml +++ b/features/rageshake/impl/src/main/res/values-zh/translations.xml @@ -1,20 +1,20 @@ "附上截图" - "如果有任何后续问题可以联系我。" + "如果您有任何后续问题,可以与我联系。" "联系我" "编辑截图" - "请尽可能详细地描述问题。你做了什么?预期会发生什么?实际上发生了什么?" + "请尽可能详细地描述问题。您做了什么?您预期会发生什么?实际发生了什么?" "描述问题…" "请尽可能用英文描述。" "描述太短,请提供详细情况。谢谢!" "发送崩溃日志" "允许日志" - "日志文件过大,无法包含在本次报告中,请通过其它方式发送给我们。" + "日志文件过大,无法包含在本报告中,请通过其他方式发送给我们。" "发送屏幕截图" - "为确认一切正常运行,日志中将包含你的消息。如要发送不含消息的日志,请关闭此设置。" - "%1$s 上次使用时曾崩溃过。是否与我们分享崩溃报告?" - "如果你遭遇通知相关问题,上传通知设置可以帮助我们调查根本原因。请注意:这些规则可能包含私人信息,例如你的显示名称或用于接收通知的关键词。" + "为确认一切正常运行,您的消息中将包含日志。如要发送不带日志的消息,请关闭此设置。" + "%1$s 上次使用时崩溃了。想和我们分享崩溃报告吗?" + "如果您遇到通知问题,上传通知设置可以帮助我们查明根本原因。" "发送通知设置" "查看日志" diff --git a/features/reportroom/impl/src/main/res/values-ca/translations.xml b/features/reportroom/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 05ca5c0fc0..0000000000 --- a/features/reportroom/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - "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-zh/translations.xml b/features/reportroom/impl/src/main/res/values-zh/translations.xml index 4366e33455..40a0b5f1b0 100644 --- a/features/reportroom/impl/src/main/res/values-zh/translations.xml +++ b/features/reportroom/impl/src/main/res/values-zh/translations.xml @@ -1,8 +1,8 @@ - "你的举报已成功提交,但在尝试退出房间时遇到问题。请重试。" + "您的报告已成功提交,但在尝试离开房间时遇到了问题。请重试。" "无法离开房间" - "向管理员举报此房间。如果消息已加密,管理员将无法读取。" - "描述举报的理由…" + "向管理员举报此房间。如果信息已加密,管理员将无法读取。" + "描述举报的原因…" "举报房间" 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 a18c82b275..59d9507571 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.performTextInput -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -23,72 +20,76 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ReportRoomViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invoke the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setReportRoomView( + rule.setReportRoomView( aReportRoomState( eventSink = eventsRecorder, ), onBackClick = it ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on report when enabled emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on report when enabled emits the expected event`() { val eventsRecorder = EventsRecorder() - setReportRoomView( + rule.setReportRoomView( aReportRoomState( reason = "Spam", eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_report) + rule.clickOn(CommonStrings.action_report) eventsRecorder.assertSingle(ReportRoomEvents.Report) } @Test - fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest { + fun `clicking on decline when disabled does not emit event`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setReportRoomView( + rule.setReportRoomView( aReportRoomState(eventSink = eventsRecorder), ) - clickOn(CommonStrings.action_report) + rule.clickOn(CommonStrings.action_report) } @Test - fun `clicking on leave room option emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on leave room option emits the expected event`() { val eventsRecorder = EventsRecorder() - setReportRoomView( + rule.setReportRoomView( aReportRoomState(eventSink = eventsRecorder), ) - clickOn(CommonStrings.action_leave_room) + rule.clickOn(CommonStrings.action_leave_room) eventsRecorder.assertSingle(ReportRoomEvents.ToggleLeaveRoom) } @Test - fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest { + fun `typing text in the reason field emits the expected Event`() { val eventsRecorder = EventsRecorder() - setReportRoomView( + rule.setReportRoomView( aReportRoomState( eventSink = eventsRecorder, reason = "" ), ) - onNodeWithText("").performTextInput("Spam!") + rule.onNodeWithText("").performTextInput("Spam!") eventsRecorder.assertSingle(ReportRoomEvents.UpdateReason("Spam!")) } } -private fun AndroidComposeUiTest.setReportRoomView( +private fun AndroidComposeTestRule.setReportRoomView( state: ReportRoomState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { 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 700a82795e..2760272d8a 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,8 +63,5 @@ 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/roles/ChangeRolesStateProvider.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt index 1ea1662a99..ebaf619d4e 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,10 +11,6 @@ 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 @@ -107,9 +103,9 @@ internal fun aChangeRolesStateWithSelectedUsers() = aChangeRolesState( internal fun aChangeRolesStateWithOwners( role: RoomMember.Role = RoomMember.Role.Admin, selectedUsers: List = listOf( - aMatrixUser(displayName = USER_NAME_ALICE), - aMatrixUser(displayName = USER_NAME_BOB), - aMatrixUser(displayName = USER_NAME_CAROL), + aMatrixUser(id = "@alice:server.org", displayName = "Alice"), + aMatrixUser(id = "@bob:server.org", displayName = "Bob"), + aMatrixUser(id = "@carol:server.org", displayName = "Carol"), ), ) = aChangeRolesState( role = role, @@ -118,22 +114,22 @@ internal fun aChangeRolesStateWithOwners( members = persistentListOf( aRoomMember( userId = UserId("@alice:server.org"), - displayName = USER_NAME_ALICE, + displayName = "Alice", role = RoomMember.Role.Owner(isCreator = true), ), aRoomMember( userId = UserId("@bob:server.org"), - displayName = USER_NAME_BOB, + displayName = "Bob", role = RoomMember.Role.Owner(isCreator = false), ), aRoomMember( userId = UserId("@carol:server.org"), - displayName = USER_NAME_CAROL, + displayName = "Carol", role = RoomMember.Role.Admin, ), aRoomMember( userId = UserId("@david:server.org"), - displayName = USER_NAME_DAVID, + displayName = "David", role = RoomMember.Role.User, ), ), 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..269fdee664 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 @@ -153,7 +153,6 @@ private fun ChangeOwnRoleBottomSheet( .navigationBarsPadding(), sheetState = sheetState, onDismissRequest = ::dismiss, - scrollable = true, ) { Text( modifier = Modifier.padding(14.dp), diff --git a/features/rolesandpermissions/impl/src/main/res/values-ca/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index eccea6f2df..0000000000 --- a/features/rolesandpermissions/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - "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 662096dd20..87936de3a7 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-cs/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-cs/translations.xml @@ -6,7 +6,6 @@ "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 02ec4ad07d..1c1f490580 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-et/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-et/translations.xml @@ -6,7 +6,6 @@ "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 4bcb764219..c526d059bc 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 fdc1c17f22..7c5d635b14 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-fi/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-fi/translations.xml @@ -6,7 +6,6 @@ "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-hu/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-hu/translations.xml index 9c88a2f20e..e44a1dba21 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-hu/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-hu/translations.xml @@ -6,7 +6,6 @@ "Ü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 ac1366d988..b1dea12151 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-it/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-it/translations.xml @@ -6,7 +6,6 @@ "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 28e1a9db89..c9410e9e3c 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml @@ -6,7 +6,6 @@ "メッセージの削除" "メンバー" "ユーザーの招待" - "ライブ位置情報を共有" "スペースの管理" "ルームを管理" "メンバーの管理" 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 9b570f5261..44659c47cd 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-pl/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-pl/translations.xml @@ -1,24 +1,17 @@ - "Administrator" + "Tylko administratorzy" "Banowanie osób" - "Zmień ustawienia" "Usuń wiadomości" - "Członek" - "Zaproś osoby" - "Udostępnij lokalizację na żywo" - "Zarządzaj przestrzeniami" - "Zarządzaj pokojami" - "Zarządzaj członkami" + "Zapraszanie osób i akceptowanie próśb o dołączenie" "Wiadomości i zawartość" - "Moderator" - "Usuń osoby" + "Administratorzy i moderatorzy" + "Usuwanie osób i odrzucanie próśb o dołączenie" "Zmień awatar pokoju" - "Edytuj szczegóły" + "Edytuj pokój" "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?" @@ -28,7 +21,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ące)" + "(Oczekujący)" "Administratorzy automatycznie mają uprawnienia moderatora" "Właściciele automatycznie mają uprawnienia administratora." "Edytuj moderatorów" @@ -38,14 +31,7 @@ "Członków" "Masz niezapisane zmiany." "Zapisać zmiany?" - "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”" + "W tym pokoju nie ma zbanowanych użytkowników." "%1$d osoba" "%1$d osoby" @@ -58,14 +44,8 @@ "Odbanuj z pokoju" "Zbanowanych" "Członków" - - "%1$d zaproszony" - "%1$d zaproszonych" - "%1$d zaproszonych" - - "Oczekuje" - "Administrator" - "Moderator" + "Tylko administratorzy" + "Administratorzy i moderatorzy" "Właściciel" "Członkowie pokoju" "Odbanowanie %1$s" @@ -78,12 +58,10 @@ "Wiadomości i zawartość" "Moderatorzy" "Właściciele" - "Uprawnienia" - "Zresetuj uprawnienia" + "Resetuj 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 962f8b2d4d..ac9ec710a2 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-ro/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-ro/translations.xml @@ -6,7 +6,6 @@ "Ș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 425da3e2ce..2b7fca0254 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-ru/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-ru/translations.xml @@ -6,7 +6,6 @@ "Удалять сообщения" "Участники" "Приглашать людей" - "Поделиться текущим местоположением" "Управление пространством" "Управление комнатами" "Управлять участниками" 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 99f21bb403..7ce3e78387 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-uk/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-uk/translations.xml @@ -38,11 +38,6 @@ "У вас є не збережені зміни." "Зберегти зміни?" "Немає заблокованих користувачів." - - "%1$d Заблокований" - "%1$d Заблоковано" - "%1$d Заблоковано" - "Перевірте правопис або спробуйте новий пошук" "Немає результатів за запитом «%1$s»" diff --git a/features/rolesandpermissions/impl/src/main/res/values-uz/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-uz/translations.xml index 5e00e72504..566ce7f53c 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-uz/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-uz/translations.xml @@ -5,7 +5,7 @@ "Sozlamalarni o‘zgartirish" "Xabarlarni olib tashlash" "A\'zo" - "Odamlarni taklif qiling" + "Odamlarni taklif qiling va qo‘shilish so‘rovlarini qabul qiling" "Maydonni boshqarish" "Xonalarni boshqarish" "A’zolarni boshqarish" diff --git a/features/rolesandpermissions/impl/src/main/res/values-vi/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-vi/translations.xml index 314b3ff6c4..5e6c736cbe 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-vi/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-vi/translations.xml @@ -29,9 +29,6 @@ "Bạn có thay đổi chưa được lưu." "Lưu thay đổi?" "Hiện không có người dùng nào bị cấm." - - "%1$d bị cấm" - "%1$d người" @@ -41,9 +38,6 @@ "Họ có thể tham gia lại phòng này nếu được mời." "Bị cấm" "Thành viên" - - "%1$d được mời" - "Quản trị viên" "Người điều hành" "Thành viên phòng" diff --git a/features/rolesandpermissions/impl/src/main/res/values-zh-rTW/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-zh-rTW/translations.xml index e9933c4819..7bc662e2fc 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-zh-rTW/translations.xml @@ -38,9 +38,6 @@ "您有尚未儲存的變更" "是否儲存變更?" "沒有被封鎖的使用者。" - - "%1$d 個已封鎖" - "檢查拼字或嘗試新搜尋" "找不到「%1$s」" @@ -53,9 +50,6 @@ "從聊天室解除封鎖" "黑名單" "成員" - - "%1$d 個已邀請" - "擱置中" "管理員" "版主" 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 53edd50a6d..d3c4cada77 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml @@ -1,31 +1,30 @@ "管理员" - "封禁人员" + "封禁成员" "更改设置" "移除消息" "成员" "邀请人员" - "共享实时位置" "管理空间" - "管理房间" + "管理聊天室" "管理成员" "消息和内容" "协管员" "移除人员" - "更改房间头像" + "更改聊天室头像" "编辑详情" - "更改房间名称" - "更改房间主题" + "更改聊天室名称" + "更改聊天室主题" "发送消息" "权限" "编辑管理员" - "此操作无法撤消。你正在提升用户的权限到与你相同的权力值。" + "您将无法撤消此操作。您正在提升用户的权限到与您相同的级别。" "添加管理员?" - "此操作无法撤消。你正在将所有权转移给所选用户。一旦离开此处,该操作将永久生效。" + "此操作无法撤销。您正在将所有权转移给所选用户。一旦离开此界面,该操作将永久生效。" "转让所有权" "降级" - "你正在降级自身,此更改无法撤消。如果你是房间中的最后一个拥有特权的用户,则无法重新获得权限。" + "您正在降级,此更改将无法撤消。如果您是聊天室中的最后一个特权用户,则无法重新获得权限。" "降级自己?" "%1$s(待处理)" "(待处理)" @@ -36,21 +35,21 @@ "管理员" "协管员" "成员" - "你有未保存的更改。" + "您有未保存的更改。" "保存更改?" - "暂无被封禁的用户。" + "没有被封禁的用户。" - "%1$d 人被封禁" + "%1$d 被禁用" "检查拼写或尝试新搜索" "未找到 “%1$s” 相关结果" "%1$d 个人" - "封禁用户" + "移除并封禁成员" "仅移除成员" - "解封" - "如果他们受到邀请,则可以重新加入房间。" + "取消封禁" + "如果受到邀请,他们可以重新加入聊天室。" "解封用户" "已封禁用户" "成员" @@ -61,10 +60,10 @@ "管理员" "协管员" "所有者" - "房间成员" + "聊天室成员" "正在解除封禁 %1$s" "管理员" - "管理员与所有者" + "管理员和所有者" "更改我的角色" "降级为成员" "降级为协管员" @@ -74,10 +73,10 @@ "所有者" "权限" "重置权限" - "重置权限后你将丢失当前设置。" + "重置权限后,您将丢失当前设置。" "重置权限?" "角色" - "房间详细信息" - "空间详细信息" + "聊天室详情" + "空间详情" "角色与权限" diff --git a/features/rolesandpermissions/impl/src/main/res/values/localazy.xml b/features/rolesandpermissions/impl/src/main/res/values/localazy.xml index dc89095786..e5ab3f1cd7 100644 --- a/features/rolesandpermissions/impl/src/main/res/values/localazy.xml +++ b/features/rolesandpermissions/impl/src/main/res/values/localazy.xml @@ -6,7 +6,6 @@ "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 72b2b8b5e7..ba7d47adb2 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 @@ -148,9 +148,7 @@ class ChangeRoomPermissionsPresenterTest { roomName = Moderator.powerLevel, roomAvatar = Moderator.powerLevel, roomTopic = Moderator.powerLevel, - spaceChild = initialPermissions.spaceChild, - beacon = initialPermissions.beacon, - beaconInfo = initialPermissions.beaconInfo, + spaceChild = initialPermissions.spaceChild ) ) } 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 668c6bb221..f28c9c150f 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.rolesandpermissions.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -26,80 +23,84 @@ import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey 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() + @Test - fun `click on back icon invokes Exit`() = runAndroidComposeUiTest { + fun `click on back icon invokes Exit`() { val recorder = EventsRecorder() - setChangeRoomPermissionsRule( + rule.setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( eventSink = recorder ) ) - pressBack() + rule.pressBack() recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `click on back key invokes Exit`() = runAndroidComposeUiTest { + fun `click on back key invokes Exit`() { val recorder = EventsRecorder() - setChangeRoomPermissionsRule( + rule.setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( eventSink = recorder ) ) - pressBackKey() + rule.pressBackKey() recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `when confirming exit with pending changes, using the back key actually exits`() = runAndroidComposeUiTest { + fun `when confirming exit with pending changes, using the back key actually exits`() { val recorder = EventsRecorder() - setChangeRoomPermissionsRule( + rule.setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, eventSink = recorder, ), ) - pressBackKey() + rule.pressBackKey() recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() = runAndroidComposeUiTest { + fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() { val recorder = EventsRecorder() - setChangeRoomPermissionsRule( + rule.setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ), ) - clickOn(CommonStrings.action_discard) + rule.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`() = runAndroidComposeUiTest { + fun `when confirming exit with pending changes, clicking on 'save' button in the dialog saves the changes`() { val recorder = EventsRecorder() - setChangeRoomPermissionsRule( + rule.setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ), ) - clickOn(CommonStrings.action_save, inDialog = true) + rule.clickOn(CommonStrings.action_save, inDialog = true) recorder.assertSingle(ChangeRoomPermissionsEvent.Save) } @Test - fun `click on a role item triggers ChangeRole event`() = runAndroidComposeUiTest { + fun `click on a role item triggers ChangeRole event`() { val recorder = EventsRecorder() - setChangeRoomPermissionsRule( + rule.setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( itemsBySection = persistentMapOf( // Makes sure there is only one item to click on @@ -108,70 +109,70 @@ class ChangeRoomPermissionsViewTest { eventSink = recorder, ) ) - clickOn(R.string.screen_room_change_permissions_room_name) - clickOn(R.string.screen_room_change_permissions_everyone) + rule.clickOn(R.string.screen_room_change_permissions_room_name) + rule.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`() = runAndroidComposeUiTest { + fun `click on the Save menu item triggers Save event`() { val recorder = EventsRecorder() - setChangeRoomPermissionsRule( + rule.setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, eventSink = recorder, ), ) - clickOn(CommonStrings.action_save) + rule.clickOn(CommonStrings.action_save) recorder.assertSingle(ChangeRoomPermissionsEvent.Save) } @Test - fun `a successful save exits the screen`() = runAndroidComposeUiTest { + fun `a successful save exits the screen`() { ensureCalledOnceWithParam(true) { callback -> - setChangeRoomPermissionsRule( + rule.setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.Success(true), ), onComplete = callback, ) - clickOn(CommonStrings.action_save) + rule.clickOn(CommonStrings.action_save) } } @Test - fun `a cancellation exits the screen`() = runAndroidComposeUiTest { + fun `a cancellation exits the screen`() { ensureCalledOnceWithParam(false) { callback -> - setChangeRoomPermissionsRule( + rule.setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.Success(false), ), onComplete = callback, ) - clickOn(CommonStrings.action_save) + rule.clickOn(CommonStrings.action_save) } } @Test - fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() = runAndroidComposeUiTest { + fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() { val recorder = EventsRecorder() - setChangeRoomPermissionsRule( + rule.setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.Failure(IllegalStateException("Failed to set room power levels")), eventSink = recorder, ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) recorder.assertSingle(ChangeRoomPermissionsEvent.ResetPendingActions) } } -private fun AndroidComposeUiTest.setChangeRoomPermissionsRule( +private fun AndroidComposeTestRule.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 62d0608f26..09bef49cbd 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,18 +6,15 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.libraries.architecture.AsyncAction @@ -33,16 +30,20 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey 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() + @Test - fun `passing a 'User' role throws an exception`() = runAndroidComposeUiTest { + fun `passing a 'User' role throws an exception`() { val exception = runCatchingExceptions { - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.User, eventSink = EnsureNeverCalledWithParam(), @@ -53,106 +54,106 @@ class ChangeRolesViewTest { } @Test - fun `back key - with search active toggles the search`() = runAndroidComposeUiTest { + fun `back key - with search active toggles the search`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, eventSink = eventsRecorder, ), ) - pressBackKey() + rule.pressBackKey() // Advance time to let the event be processed, as the search toggle might have some delay (e.g. for the animation) - mainClock.advanceTimeBy(1) + rule.mainClock.advanceTimeBy(1) eventsRecorder.assertSingle(ChangeRolesEvent.ToggleSearchActive) } @Test - fun `back key - with search inactive exits the screen`() = runAndroidComposeUiTest { + fun `back key - with search inactive exits the screen`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( isSearchActive = false, eventSink = eventsRecorder, ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test - fun `back button - exits the screen`() = runAndroidComposeUiTest { + fun `back button - exits the screen`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( isSearchActive = false, eventSink = eventsRecorder, ), ) - pressBack() + rule.pressBack() eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test - fun `save button - with changes, it saves them`() = runAndroidComposeUiTest { + fun `save button - with changes, it saves them`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( hasPendingChanges = true, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_save) + rule.clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save button - with no changes, does nothing`() = runAndroidComposeUiTest { + fun `save button - with no changes, does nothing`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( hasPendingChanges = false, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_save) + rule.clickOn(CommonStrings.action_save) eventsRecorder.assertEmpty() } @Test - fun `exit confirmation dialog - discard exits the screen`() = runAndroidComposeUiTest { + fun `exit confirmation dialog - discard exits the screen`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, savingState = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_discard) + rule.clickOn(CommonStrings.action_discard) eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test - fun `exit confirmation dialog - save emits the save event`() = runAndroidComposeUiTest { + fun `exit confirmation dialog - save emits the save event`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, savingState = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_save) + rule.clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save admins confirmation dialog - submit saves the changes`() = runAndroidComposeUiTest { + fun `save admins confirmation dialog - submit saves the changes`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Admin, isSearchActive = true, @@ -160,14 +161,14 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save owners confirmation dialog - continue saves the changes`() = runAndroidComposeUiTest { + fun `save owners confirmation dialog - continue saves the changes`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Owner(isCreator = false), isSearchActive = true, @@ -175,14 +176,14 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save admins confirmation dialog - cancel removes the dialog`() = runAndroidComposeUiTest { + fun `save admins confirmation dialog - cancel removes the dialog`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Admin, isSearchActive = true, @@ -190,14 +191,14 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) } @Test - fun `save owners confirmation dialog - cancel removes the dialog`() = runAndroidComposeUiTest { + fun `save owners confirmation dialog - cancel removes the dialog`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Owner(isCreator = false), isSearchActive = true, @@ -205,39 +206,39 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) } @Test - fun `error dialog - dismissing removes the dialog`() = runAndroidComposeUiTest { + fun `error dialog - dismissing removes the dialog`() { val eventsRecorder = EventsRecorder() - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, savingState = AsyncAction.Failure(IllegalStateException("boom")), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) } @Test - fun `testing removing user from selected list emits the expected event`() = runAndroidComposeUiTest { + fun `testing removing user from selected list emits the expected event`() { val eventsRecorder = EventsRecorder() val selectedUsers = aMatrixUserList().take(2) val userToDeselect = selectedUsers[1] assertThat(userToDeselect.displayName).isEqualTo("Bob") - setChangeRolesContent( + rule.setChangeRolesContent( state = aChangeRolesStateWithSelectedUsers().copy( selectedUsers = selectedUsers.toImmutableList(), eventSink = eventsRecorder, ), ) // Unselect the user from the row list - val contentDescription = activity!!.getString(CommonStrings.action_remove) - onNodeWithContentDescription( + val contentDescription = rule.activity.getString(CommonStrings.action_remove) + rule.onNodeWithContentDescription( label = contentDescription, useUnmergedTree = true, ).performClick() @@ -246,7 +247,7 @@ class ChangeRolesViewTest { @Test @Config(qualifiers = "h1000dp") - fun `testing adding user to the selected list emits the expected event`() = runAndroidComposeUiTest { + fun `testing adding user to the selected list emits the expected event`() { val eventsRecorder = EventsRecorder() val selectedUsers = aMatrixUserList().take(2) val state = aChangeRolesStateWithSelectedUsers().copy( @@ -255,16 +256,16 @@ class ChangeRolesViewTest { ) val userToSelect = (state.searchResults as SearchBarResultState.Results).results.members.first().toMatrixUser() assertThat(userToSelect.displayName).isEqualTo("Carol") - setChangeRolesContent( + rule.setChangeRolesContent( state = state, ) // Select the user from the user list - onNodeWithText("Carol").performClick() + rule.onNodeWithText("Carol").performClick() eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect)) } @Test - fun `testing removing user to the selected list emits the expected event`() = runAndroidComposeUiTest { + fun `testing removing user to the selected list emits the expected event`() { val eventsRecorder = EventsRecorder() val selectedUsers = aMatrixUserList().take(2) val state = aChangeRolesStateWithSelectedUsers().copy( @@ -273,18 +274,18 @@ class ChangeRolesViewTest { ) val userToSelect = (state.searchResults as SearchBarResultState.Results).results.moderators.first().toMatrixUser() assertThat(userToSelect.displayName).isEqualTo("Bob") - setChangeRolesContent( + rule.setChangeRolesContent( state = state, ) // Unselect the user from the user list - onAllNodesWithText( + rule.onAllNodesWithText( text = "Bob", useUnmergedTree = true, )[1].performClick() eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect)) } - private fun AndroidComposeUiTest.setChangeRolesContent( + private fun AndroidComposeTestRule.setChangeRolesContent( state: ChangeRolesState, ) { setContent { 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 d8908c405d..e08ae205b7 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.rolesandpermissions.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -26,154 +23,159 @@ 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.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() + @Test - fun `click on back invokes expected callback`() = runAndroidComposeUiTest { + fun `click on back invokes expected callback`() { ensureCalledOnce { callback -> - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( goBack = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `tapping on Admins opens admin list`() = runAndroidComposeUiTest { + fun `tapping on Admins opens admin list`() { ensureCalledOnce { callback -> - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( aRolesAndPermissionsState( roomSupportsOwners = false, eventSink = EventsRecorder(expectEvents = false) ), openAdminList = callback, ) - clickOn(R.string.screen_room_roles_and_permissions_admins) + rule.clickOn(R.string.screen_room_roles_and_permissions_admins) } } @Test - fun `tapping on Admins and Owners opens admin list`() = runAndroidComposeUiTest { + fun `tapping on Admins and Owners opens admin list`() { ensureCalledOnce { callback -> - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( aRolesAndPermissionsState( roomSupportsOwners = true, eventSink = EventsRecorder(expectEvents = false) ), openAdminList = callback, ) - clickOn(R.string.screen_room_roles_and_permissions_admins_and_owners) + rule.clickOn(R.string.screen_room_roles_and_permissions_admins_and_owners) } } @Test - fun `tapping on Moderators opens moderators list`() = runAndroidComposeUiTest { + fun `tapping on Moderators opens moderators list`() { ensureCalledOnce { callback -> - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( openModeratorList = callback, ) - clickOn(R.string.screen_room_roles_and_permissions_moderators) + rule.clickOn(R.string.screen_room_roles_and_permissions_moderators) } } @Test @Config(qualifiers = "h640dp") - fun `tapping permission item open the change permissions screen`() = runAndroidComposeUiTest { + fun `tapping permission item open the change permissions screen`() { ensureCalledTimes(1) { callback -> - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( openEditPermissions = callback, ) - clickOn(R.string.screen_room_roles_and_permissions_permissions_header) + rule.clickOn(R.string.screen_room_roles_and_permissions_permissions_header) } } @Test @Config(qualifiers = "h640dp") - fun `tapping on reset permissions triggers ResetPermissions event`() = runAndroidComposeUiTest { + fun `tapping on reset permissions triggers ResetPermissions event`() { val recorder = EventsRecorder() - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( state = aRolesAndPermissionsState( eventSink = recorder, ), ) - clickOn(R.string.screen_room_roles_and_permissions_reset) + rule.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`() = runAndroidComposeUiTest { + fun `tapping on Reset in the reset permissions confirmation dialog triggers ResetPermissions event`() { val recorder = EventsRecorder() - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( state = aRolesAndPermissionsState( resetPermissionsAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - clickOn(CommonStrings.action_reset) + rule.clickOn(CommonStrings.action_reset) recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions) } @Test - fun `tapping on Cancel in the reset permissions confirmation dialog triggers CancelPendingAction event`() = runAndroidComposeUiTest { + fun `tapping on Cancel in the reset permissions confirmation dialog triggers CancelPendingAction event`() { val recorder = EventsRecorder() - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( state = aRolesAndPermissionsState( resetPermissionsAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - clickOn(CommonStrings.action_cancel) + rule.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`() = runAndroidComposeUiTest { + fun `tapping on 'Demote to moderator' in the demote self bottom sheet triggers the right event`() { val recorder = EventsRecorder() - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator) - mainClock.advanceTimeBy(1_000L) + rule.clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator) + rule.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`() = runAndroidComposeUiTest { + fun `tapping on 'Demote to member' in the demote self bottom sheet triggers the right event`() = runTest { val recorder = EventsRecorder() - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_member) - mainClock.advanceTimeBy(1_000L) + rule.clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_member) + rule.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`() = runAndroidComposeUiTest { + fun `tapping on 'Cancel' in the demote self bottom sheet triggers the right event`() { val recorder = EventsRecorder() - setRolesAndPermissionsView( + rule.setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - clickOn(CommonStrings.action_cancel) - mainClock.advanceTimeBy(1_000L) + rule.clickOn(CommonStrings.action_cancel) + rule.mainClock.advanceTimeBy(1_000L) recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction) } } -private fun AndroidComposeUiTest.setRolesAndPermissionsView( +private fun AndroidComposeTestRule.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 deleted file mode 100644 index ea90cabc34..0000000000 --- a/features/roomaliasresolver/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "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/main/res/values-zh/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-zh/translations.xml index 6b44301045..52934b6a08 100644 --- a/features/roomaliasresolver/impl/src/main/res/values-zh/translations.xml +++ b/features/roomaliasresolver/impl/src/main/res/values-zh/translations.xml @@ -1,5 +1,5 @@ "无法显示此房间预览" - "无法解析房间别名。" + "无法解析聊天室别名。" 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 5f871183d6..4b37f993f9 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias @@ -25,44 +22,48 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomAliasHelperViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setRoomAliasResolverView( + rule.setRoomAliasResolverView( aRoomAliasResolverState( eventSink = eventsRecorder, ), onBackClick = it ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on Retry emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on Retry emits the expected Event`() { val eventsRecorder = EventsRecorder() - setRoomAliasResolverView( + rule.setRoomAliasResolverView( aRoomAliasResolverState( resolveState = AsyncData.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_retry) + rule.clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(RoomAliasResolverEvents.Retry) } @Test - fun `success state invokes the expected Callback`() = runAndroidComposeUiTest { + fun `success state invokes the expected Callback`() { val result = aResolvedRoomAlias() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(result) { - setRoomAliasResolverView( + rule.setRoomAliasResolverView( aRoomAliasResolverState( resolveState = AsyncData.Success(result), eventSink = eventsRecorder, @@ -73,7 +74,7 @@ class RoomAliasHelperViewTest { } } -private fun AndroidComposeUiTest.setRoomAliasResolverView( +private fun AndroidComposeTestRule.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 15e320c8f8..3d863321e3 100644 --- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt @@ -23,6 +23,7 @@ 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 0612adbea1..4c6fcf8e59 100644 --- a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt +++ b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt @@ -89,10 +89,10 @@ class RoomCallStatePresenterTest { } @Test - fun `present - initial state - when is DM room`() = runTest { + fun `present - initial state - when is direct room`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - initialRoomInfo = aRoomInfo(isDm = true), + initialRoomInfo = aRoomInfo(isDirect = 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 3474711b55..07e10c65ef 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -38,10 +38,9 @@ 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, clearBackStack: Boolean = false) + fun navigateToRoom(roomId: RoomId, serverNames: List) 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 77e2d0c229..2765a95a1e 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -40,7 +40,6 @@ 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) @@ -70,7 +69,6 @@ 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 a82766175d..de801e8aae 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,6 +14,4 @@ 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 d4108a77e3..c3ae902ba9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -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.CallData +import io.element.android.features.call.api.CallType 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,10 +176,6 @@ class RoomDetailsFlowNode( return when (navTarget) { NavTarget.RoomDetails -> { val roomDetailsCallback = object : RoomDetailsNode.Callback { - override fun navigateBack() { - callback.onDone() - } - override fun navigateToRoomMemberList() { backstack.push(NavTarget.RoomMemberList) } @@ -229,13 +225,13 @@ class RoomDetailsFlowNode( } override fun navigateToRoomCall(callIntent: CallIntent) { - val callData = CallData( + val inputs = CallType.RoomCall( sessionId = room.sessionId, roomId = room.roomId, isAudioCall = callIntent == CallIntent.AUDIO ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - elementCallEntryPoint.startCall(callData) + elementCallEntryPoint.startCall(inputs) } override fun navigateToReportRoom() { @@ -267,20 +263,7 @@ class RoomDetailsFlowNode( } NavTarget.InviteMembers -> { - 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)) + createNode(buildContext) } is NavTarget.RoomNotificationSettings -> { @@ -305,7 +288,7 @@ class RoomDetailsFlowNode( override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) { elementCallEntryPoint.startCall( - CallData( + CallType.RoomCall( 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 deleted file mode 100644 index 9e2b77a260..0000000000 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNavigator.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 7f22c7e111..e9aad5b1d2 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,13 +42,12 @@ import io.element.android.libraries.androidutils.R as AndroidUtilsR class RoomDetailsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - presenterFactory: RoomDetailsPresenter.Factory, + private val presenter: RoomDetailsPresenter, private val room: BaseRoom, private val analyticsService: AnalyticsService, private val leaveRoomRenderer: LeaveRoomRenderer, -) : Node(buildContext, plugins = plugins), RoomDetailsNavigator { +) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { - fun navigateBack() fun navigateToRoomMemberList() fun navigateToInviteMembers() fun navigateToRoomDetailsEdit() @@ -66,7 +65,6 @@ class RoomDetailsNode( fun navigateToSelectNewOwnersWhenLeaving() } - private val presenter = presenterFactory.create(this) private val callback: Callback = callback() init { @@ -146,8 +144,4 @@ 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 e6617d1dd5..853d76859b 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,9 +17,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import dev.zacsweers.metro.Assisted -import dev.zacsweers.metro.AssistedFactory -import dev.zacsweers.metro.AssistedInject +import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.knockrequests.api.KnockRequestPermissions import io.element.android.features.knockrequests.api.knockRequestPermissions @@ -37,35 +35,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.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.api.timeline.ReceiptType +import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember 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 -@AssistedInject +@Inject 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, @@ -74,16 +73,7 @@ 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() @@ -95,14 +85,6 @@ 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 } } @@ -117,8 +99,9 @@ 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) + val roomType = getRoomType(dmMember, currentMember) val roomCallState = roomCallStatePresenter.present() val joinedMemberCount by remember { derivedStateOf { roomInfo.joinedMembersCount } } @@ -131,11 +114,14 @@ 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 { permissions.knockRequestsPermissions.hasAny && joinRule == JoinRule.Knock } + derivedStateOf { isKnockRequestsEnabled && permissions.knockRequestsPermissions.hasAny && joinRule == JoinRule.Knock } } val canShowSecurityAndPrivacy by remember { derivedStateOf { !isDm && permissions.securityAndPrivacyPermissions.hasAny(isSpace = false, joinRule = joinRule) } @@ -161,7 +147,7 @@ class RoomDetailsPresenter( } RoomDetailsEvent.UnmuteNotification -> { scope.launch(dispatchers.io) { - notificationSettingsService.unmuteRoom(room.roomId, isEncrypted, room.isDm()) + notificationSettingsService.unmuteRoom(room.roomId, isEncrypted, room.isOneToOne) } } is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite) @@ -169,8 +155,6 @@ class RoomDetailsPresenter( clipboardHelper.copyPlainText(event.text) snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) } - is RoomDetailsEvent.MarkAsRead -> scope.markAsRead() - is RoomDetailsEvent.MarkAsUnread -> scope.markAsUnread() } } @@ -184,6 +168,8 @@ class RoomDetailsPresenter( val canReportRoom by produceState(false) { value = client.canReportRoom() } + val enableKeyShareOnInvite by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false) + return RoomDetailsState( roomId = room.roomId, roomName = roomName, @@ -202,7 +188,7 @@ class RoomDetailsPresenter( isFavorite = isFavorite, displayRolesAndPermissionsSettings = !isDm && permissions.canEditRolesAndPermissions, isPublic = joinRule == JoinRule.Public, - heroes = roomInfo.heroes, + heroes = roomInfo.heroes.toImmutableList(), pinnedMessagesCount = pinnedMessagesCount, snackbarMessage = snackbarMessage, canShowKnockRequests = canShowKnockRequests, @@ -213,8 +199,8 @@ class RoomDetailsPresenter( isTombstoned = roomInfo.successorRoom != null, showDebugInfo = isDeveloperModeEnabled, roomVersion = roomInfo.roomVersion, + enableKeyShareOnInvite = enableKeyShareOnInvite, roomHistoryVisibility = roomInfo.historyVisibility, - hasNewContent = hasNewContent, eventSink = ::handleEvent, ) } @@ -227,9 +213,15 @@ class RoomDetailsPresenter( } @Composable - private fun getRoomType(dmMember: RoomMember?): RoomDetailsType = remember(dmMember) { - if (dmMember != null) { - RoomDetailsType.Dm(otherMember = dmMember) + private fun getRoomType( + dmMember: RoomMember?, + currentMember: RoomMember?, + ): RoomDetailsType = remember(dmMember, currentMember) { + if (dmMember != null && currentMember != null) { + RoomDetailsType.Dm( + me = currentMember, + otherMember = dmMember, + ) } else { RoomDetailsType.Room } @@ -268,26 +260,4 @@ 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 bd3b90d416..20ec12fdb9 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 @@ -51,8 +51,8 @@ data class RoomDetailsState( val isTombstoned: Boolean, val showDebugInfo: Boolean, val roomVersion: String?, + val enableKeyShareOnInvite: Boolean, val roomHistoryVisibility: RoomHistoryVisibility, - val hasNewContent: Boolean, val eventSink: (RoomDetailsEvent) -> Unit ) { val roomBadges = buildList { @@ -64,7 +64,7 @@ data class RoomDetailsState( if (isPublic) { add(RoomBadge.PUBLIC) } - if (isEncrypted) { + if (enableKeyShareOnInvite && isEncrypted) { when (roomHistoryVisibility) { RoomHistoryVisibility.Invited, RoomHistoryVisibility.Joined -> add(RoomBadge.SHARED_HISTORY_HIDDEN) RoomHistoryVisibility.Shared -> add(RoomBadge.SHARED_HISTORY_SHARED) @@ -78,7 +78,10 @@ data class RoomDetailsState( @Immutable sealed interface RoomDetailsType { data object Room : RoomDetailsType - data class Dm(val otherMember: RoomMember) : RoomDetailsType + data class Dm( + val me: RoomMember, + 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 7182d2ec04..e40e6b03ef 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,6 +13,7 @@ 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 @@ -34,7 +35,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider override val values: Sequence get() = sequenceOf( aRoomDetailsState(displayAdminSettings = true), - aRoomDetailsState(roomTopic = RoomTopicState.Hidden, showDebugInfo = true, hasNewContent = true), + aRoomDetailsState(roomTopic = RoomTopicState.Hidden, showDebugInfo = true), aRoomDetailsState(roomTopic = RoomTopicState.CanAddTopic), aRoomDetailsState(isEncrypted = false), aRoomDetailsState(roomAlias = null), @@ -74,7 +75,6 @@ fun aDmRoomMember( isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, - isServiceMember: Boolean = false, ) = RoomMember( userId = userId, displayName = displayName, @@ -84,8 +84,7 @@ fun aDmRoomMember( powerLevel = powerLevel, isIgnored = isIgnored, role = role, - membershipChangeReason = membershipChangeReason, - isServiceMember = isServiceMember, + membershipChangeReason = membershipChangeReason ) fun aRoomDetailsState( @@ -122,8 +121,8 @@ fun aRoomDetailsState( canReportRoom: Boolean = true, isTombstoned: Boolean = false, showDebugInfo: Boolean = false, + enableKeyShareOnInvite: Boolean = false, roomHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Shared, - hasNewContent: Boolean = false, eventSink: (RoomDetailsEvent) -> Unit = {}, ) = RoomDetailsState( roomId = roomId, @@ -154,8 +153,8 @@ fun aRoomDetailsState( isTombstoned = isTombstoned, showDebugInfo = showDebugInfo, roomVersion = "12", + enableKeyShareOnInvite = enableKeyShareOnInvite, roomHistoryVisibility = roomHistoryVisibility, - hasNewContent = hasNewContent, eventSink = eventSink, ) @@ -182,8 +181,10 @@ fun aDmRoomDetailsState( roomName = roomName, isPublic = false, isEncrypted = isEncrypted, - canInvite = true, - roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)), + roomType = RoomDetailsType.Dm( + me = aRoomMember(), + otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored), + ), roomMemberDetailsState = aUserProfileState( isBlocked = AsyncData.Success(isDmMemberIgnored), verificationState = dmRoomMemberVerificationState, @@ -194,5 +195,6 @@ fun aSharedHistoryRoomDetailsState( roomHistoryVisibility: RoomHistoryVisibility ) = aRoomDetailsState( isEncrypted = true, + enableKeyShareOnInvite = true, roomHistoryVisibility = roomHistoryVisibility, ) 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 b30c73b9c8..c48716db11 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,7 +8,6 @@ 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 @@ -22,7 +21,6 @@ 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 @@ -33,7 +31,6 @@ 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 @@ -55,6 +52,7 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.avatar.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 @@ -93,7 +91,6 @@ 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 @Composable @@ -157,9 +154,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) }, @@ -189,46 +186,6 @@ fun RoomDetailsView( ) } - PreferenceCategory { - 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) - ) - }, - ) - } 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()) - ), - ) - } - } PreferenceCategory { if (state.roomNotificationSettings != null) { NotificationItem( @@ -249,15 +206,8 @@ fun RoomDetailsView( onClick = onSecurityAndPrivacyClick ) } - } - state.roomMemberDetailsState?.let { dmMemberDetails -> - if (state.canInvite) { - PreferenceCategory { - InviteItem(onClick = invitePeople) - } - } - PreferenceCategory { + state.roomMemberDetailsState?.let { dmMemberDetails -> ProfileItem( verificationState = dmMemberDetails.verificationState, onClick = { onProfileClick(dmMemberDetails.userId) } @@ -422,14 +372,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), @@ -467,7 +417,6 @@ private fun RoomHeaderSection( ), contentDescription = stringResource(CommonStrings.a11y_room_avatar), modifier = Modifier - .clip(CircleShape) .clickable( enabled = avatarUrl != null, onClickLabel = stringResource(CommonStrings.action_view), @@ -486,9 +435,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 @@ -499,24 +448,11 @@ private fun DmHeaderSection( .padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - 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) + DmAvatars( + userAvatarData = me.getAvatarData(size = AvatarSize.DmCluster), + otherUserAvatarData = otherMember.getAvatarData(size = AvatarSize.DmCluster), + openAvatarPreview = { url -> openAvatarPreview(me.getBestName(), url) }, + openOtherAvatarPreview = { url -> openAvatarPreview(roomName, url) }, ) TitleAndSubtitle( title = roomName, @@ -741,17 +677,6 @@ 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?, 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 3919817313..ea0ed1bb72 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,7 +11,6 @@ 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 @@ -20,16 +19,10 @@ 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) @@ -42,10 +35,6 @@ class RoomInviteMembersNode( room: JoinedRoom, invitePeoplePresenterFactory: InvitePeoplePresenter.Factory, ) : Node(buildContext, plugins = plugins) { - interface Callback : Plugin { - fun openCreatedRoom(roomId: RoomId) - } - init { lifecycle.subscribe( onResume = { @@ -59,8 +48,6 @@ class RoomInviteMembersNode( roomId = room.roomId, ) - private val callback = plugins.callback() - @Composable override fun View(modifier: Modifier) { val state = invitePeoplePresenter.present() @@ -72,19 +59,6 @@ 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/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 23c6292294..bc077feb6a 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,15 +15,6 @@ 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 @@ -128,7 +119,6 @@ fun aRoomMember( isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, - isServiceMember: Boolean = false, ) = RoomMember( userId = userId, displayName = displayName, @@ -139,7 +129,6 @@ fun aRoomMember( isIgnored = isIgnored, role = role, membershipChangeReason = membershipChangeReason, - isServiceMember = isServiceMember, ) fun aRoomMemberList() = persistentListOf( @@ -154,21 +143,21 @@ fun aRoomMemberList() = persistentListOf( aBannedMallory(), ) -fun anEve(): RoomMember = aRoomMember(UserId("@eve:server.org"), USER_NAME_EVE) +fun anEve(): RoomMember = aRoomMember(UserId("@eve:server.org"), "Eve") -fun aDavid(): RoomMember = aRoomMember(UserId("@david:server.org"), USER_NAME_DAVID) +fun aDavid(): RoomMember = aRoomMember(UserId("@david:server.org"), "David") -fun aCarol(): RoomMember = aRoomMember(UserId("@carol:server.org"), USER_NAME_CAROL) +fun aCarol(): RoomMember = aRoomMember(UserId("@carol:server.org"), "Carol") -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 anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.Admin) +fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.Moderator) -fun anInvitedVictor() = aRoomMember(UserId("@victor:server.org"), USER_NAME_VICTOR, membership = RoomMembershipState.INVITE) +fun anInvitedVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE) -fun anInvitedWalter() = aRoomMember(UserId("@walter:server.org"), USER_NAME_WALTER, membership = RoomMembershipState.INVITE) +fun anInvitedWalter() = aRoomMember(UserId("@walter:server.org"), "Walter", membership = RoomMembershipState.INVITE) -fun aBannedSusie(): RoomMember = aRoomMember(UserId("@susie:server.org"), USER_NAME_SUSIE, membership = RoomMembershipState.BAN) +fun aBannedSusie(): RoomMember = aRoomMember(UserId("@susie:server.org"), "Susie", membership = RoomMembershipState.BAN) -fun aBannedMallory(): RoomMember = aRoomMember(UserId("@mallory:server.org"), USER_NAME_MALLORY, membership = RoomMembershipState.BAN) +fun aBannedMallory(): RoomMember = aRoomMember(UserId("@mallory:server.org"), "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/notificationsettings/RoomNotificationSettingsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt index 08dbf2019d..c7930b5895 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.isDm()).getOrThrow() + notificationSettingsService.getRoomNotificationSettings(room.roomId, isEncrypted, room.isOneToOne).getOrThrow() }.runCatchingUpdatingState(roomNotificationSettings) } @@ -170,7 +170,7 @@ class RoomNotificationSettingsPresenter( val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( isEncrypted, - room.isDm() + room.isOneToOne ).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 69c5be0021..9b89f2cef0 100644 --- a/features/roomdetails/impl/src/main/res/values-be/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-be/translations.xml @@ -43,7 +43,6 @@ "Не атрымалася адключыць гук у гэтым пакоі, паўтарыце спробу." "Не ўдалося ўключыць гук у гэтым пакоі. Паўтарыце спробу." "Запрасіць карыстальнікаў" - "Запрасіць" "Пакінуць размову" "Пакінуць пакой" "Уласныя" @@ -100,6 +99,4 @@ "Ролі" "Дэталі пакоя" "Ролі і дазволы" - "Пазначыць як прачытанае" - "Пазначыць як непрачытанае" 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 7369d78f1d..55bce6cd12 100644 --- a/features/roomdetails/impl/src/main/res/values-bg/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-bg/translations.xml @@ -33,7 +33,6 @@ "Неуспешно заглушаване на тази стая, моля, опитайте отново." "Неуспешно раззаглушаване на тази стая, моля, опитайте отново." "Поканване на хора" - "Поканване" "Напускане на разговора" "Напускане на стаята" "Медия и файлове" @@ -79,8 +78,6 @@ "Роли" "Подробности за стаята" "Роли и разрешения" - "Отбелязване като прочетено" - "Отбелязване като непрочетено" "Добавяне на адрес" "Да, включване на шифроването" "Да се включи ли шифроването?" diff --git a/features/roomdetails/impl/src/main/res/values-ca/translations.xml b/features/roomdetails/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 8b9f8d937e..0000000000 --- a/features/roomdetails/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,141 +0,0 @@ - - - "É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 190e1ae5de..eaf787e4e3 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -56,7 +56,6 @@ "Nezavírejte aplikaci, dokud neskončíte." "Příprava pozvánek…" "Pozvat přátele" - "Pozvat" "Opustit konverzaci" "Opustit místnost" "Média a soubory" @@ -135,8 +134,6 @@ "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 f5d4eecac5..be8ab00639 100644 --- a/features/roomdetails/impl/src/main/res/values-cy/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cy/translations.xml @@ -53,7 +53,6 @@ "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" @@ -120,8 +119,6 @@ "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 191df9f059..e381a30957 100644 --- a/features/roomdetails/impl/src/main/res/values-da/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-da/translations.xml @@ -56,7 +56,6 @@ "Luk ikke appen, før den er færdig." "Forbereder invitationer…" "Invitér andre" - "Invitér" "Forlad samtalen" "Forlad rum" "Medier og filer" @@ -132,8 +131,6 @@ "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." @@ -156,7 +153,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 klyngen" + "Medlemmer af rummet" "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 08201fdc34..33f686332f 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -1,8 +1,5 @@ - "Neue Mitglieder sehen den Nachrichtenverlauf nicht" - "Neue Mitglieder sehen den Nachrichtenverlauf" - "Jeder sieht den Nachrichtenverlauf" "Du benötigst eine Chat-Adresse, um den Chat im öffentlichen Verzeichnis sichtbar zu machen." "Chat-Adresse bearbeiten" "Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten." @@ -56,7 +53,6 @@ "Schließ die App erst, wenn du fertig bist." "Einladungen werden vorbereitet…" "Nutzer einladen" - "Einladen" "Unterhaltung verlassen" "Verlassen" "Medien und Dateien" @@ -132,8 +128,6 @@ "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." @@ -156,7 +150,6 @@ Wir empfehlen keine Verschlüsselung für Chats zu aktivieren, die jeder finden "Zugang" "Jeder in autorisierten Spaces kann beitreten." "Jeder in %1$s kann beitreten." - "Space Mitglieder" "Spaces werden zur Zeit nicht unterstützt." "Du benötigst eine Chat-Adresse, um den Chat im öffentlichen Verzeichnis sichtbar zu machen." "Adresse" 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 5f1a3ce4ab..c1b108a524 100644 --- a/features/roomdetails/impl/src/main/res/values-el/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-el/translations.xml @@ -56,7 +56,6 @@ "Μην κλείσετε την εφαρμογή μέχρι να τελειώσει." "Προετοιμασία προσκλήσεων…" "Πρόσκληση ατόμων" - "Πρόσκληση" "Αποχώρηση από τη συζήτηση" "Αποχώρηση από την αίθουσα" "Πολυμέσα και αρχεία" @@ -132,8 +131,6 @@ "Ρόλοι" "Λεπτομέρειες αίθουσας" "Ρόλοι και δικαιώματα" - "Επισήμανση ως αναγνωσμένου" - "Επισήμανση ως μη αναγνωσμένου" "Προσθήκη διεύθυνσης" "Οποιοσδήποτε σε εξουσιοδοτημένους χώρους μπορεί να συμμετάσχει, αλλά όλοι οι άλλοι πρέπει να ζητήσουν πρόσβαση." "Όλοι πρέπει να αιτούνται πρόσβαση." 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 9537596d4e..716e3ed32a 100644 --- a/features/roomdetails/impl/src/main/res/values-es/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml @@ -47,7 +47,6 @@ "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" @@ -107,8 +106,6 @@ "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 e6962456ac..d5d3af8563 100644 --- a/features/roomdetails/impl/src/main/res/values-et/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-et/translations.xml @@ -56,7 +56,6 @@ "Ära sulge rakendust enne, kui tegevus on lõppenud." "Valmistan kutseid ette…" "Kutsu osalejaid" - "Kutsu" "Lahku vestlusest" "Lahku jututoast" "Meedia ja failid" @@ -132,8 +131,6 @@ "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 64f32e44d9..7fb07cf2a4 100644 --- a/features/roomdetails/impl/src/main/res/values-eu/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-eu/translations.xml @@ -44,7 +44,6 @@ "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" @@ -100,8 +99,6 @@ "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 af0cbbe29b..bfe743fd1b 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,7 +51,6 @@ "کاره را تا زمان پایانش نبندید." "آماده سازی دعوت‌ها…" "دعوت افراد" - "دعوت" "ترک گفت‌وگو" "ترک اتاق" "رسانه‌ها و پرونده‌ها" @@ -61,7 +60,7 @@ "پیام‌های سنجاق شده" "نمایه" "درخواست‌های پیوستن" - "نقش‌ها و مجوزها" + "نقش‌ها و اجازه‌ها" "امنیت و محرمانگی" "امنیت" "هم‌رسانی اتاق" @@ -80,8 +79,8 @@ "تحریم نکردن از اتاق" "محروم" "اعضا" - "ادمین" - "ناظم" + "فقط مدیران" + "مدیرن و ناظران" "مالک" "اعضای اتاق" "رفع تحریم %1$s" @@ -112,9 +111,7 @@ "بازنشانی اجازه‌ها؟" "نقش‌ها" "جزییات اتاق" - "نقش‌ها و مجوزها" - "علامت‌گذاری به عنوان خوانده شده" - "نشان به ناخوانده" + "نقش‌ها و اجازه‌ها" "افزودن نشانی اتاق" "درخواست دعوت" "بله. به کار انداختن رمزنگاری" 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 4eafca087e..110790fac1 100644 --- a/features/roomdetails/impl/src/main/res/values-fi/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fi/translations.xml @@ -56,7 +56,6 @@ "Älä sulje sovellusta ennen kuin se on valmis." "Valmistellaan kutsuja…" "Kutsu henkilöitä" - "Kutsu" "Poistu keskustelusta" "Poistu huoneesta" "Media ja tiedostot" @@ -132,8 +131,6 @@ "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 0d6844cb7e..0fad68c934 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -56,7 +56,6 @@ "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" @@ -132,8 +131,6 @@ "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 5619164dd2..09ec7432df 100644 --- a/features/roomdetails/impl/src/main/res/values-hr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-hr/translations.xml @@ -1,8 +1,5 @@ - "član" - "Novi članovi vide povijest" - "Svatko može vidjeti povijest" "Trebat će vam adresa kako bi bila vidljiva u javnom direktoriju." "Uredi adresu" "Došlo je do pogreške prilikom ažuriranja postavke obavijesti." @@ -56,7 +53,6 @@ "Ne zatvarajte aplikaciju dok se ne završi." "Priprema pozivnica…" "Pozovi osobe" - "Pozovi" "Napusti razgovor" "Napusti sobu" "Mediji i datoteke" @@ -135,12 +131,9 @@ "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." - "Zatraži pridruživanje" "Svatko u %1$s može se pridružiti, ali svi ostali moraju zatražiti pristup." "Da, omogući šifriranje" "Nakon što se šifriranje za sobu omogući, više se neće moći onemogućiti. Povijest poruka bit će vidljiva samo članovima sobe otkad su pozvani ili otkad su joj se pridružili. @@ -151,15 +144,13 @@ Ne preporučujemo omogućavanje šifriranja za sobe koje svatko može pronaći i "Šifriranje" "Omogući sveobuhvatno šifriranje" "Svatko se može pridružiti." - "Bilo tko" - "Odaberite iz kojih se prostora članovi mogu pridružiti ovoj sobi bez pozivnice. %1$s" + "Odaberite iz kojih se prostora članovi mogu pridružiti ovoj sobi bez pozivnice. %1$s" "Upravljaj prostorima" "Samo pozvane osobe mogu se pridružiti." "Samo s pozivnicom" "Pristup" "Svatko tko se nalazi u ovlaštenim prostorima može se pridružiti." "Svatko u %1$s može se pridružiti." - "Članovi prostora" "Prostori trenutačno nisu podržani" "Trebat će vam adresa kako bi bila vidljiva u javnom direktoriju." "Adresa" 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 fed765fb45..1dc523f319 100644 --- a/features/roomdetails/impl/src/main/res/values-hu/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-hu/translations.xml @@ -56,7 +56,6 @@ "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" @@ -132,8 +131,6 @@ "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 d7403329c4..41bf3d2826 100644 --- a/features/roomdetails/impl/src/main/res/values-in/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-in/translations.xml @@ -53,7 +53,6 @@ "Jangan tutup aplikasi tunggu hingga selesai." "Mempersiapkan undangan…" "Undang orang-orang" - "Undang" "Tinggalkan percakapan" "Tinggalkan ruangan" "Media dan berkas" @@ -115,8 +114,6 @@ "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 b8a18d4421..22ec99f2b5 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -56,7 +56,6 @@ "Non chiudere l\'app fino al completamento." "Preparazione degli inviti…" "Invita persone" - "Invita" "Abbandona la conversazione" "Esci dalla stanza" "File e contenuti multimediali" @@ -132,8 +131,6 @@ "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 5a65f1a404..70b3255437 100644 --- a/features/roomdetails/impl/src/main/res/values-ja/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ja/translations.xml @@ -56,7 +56,6 @@ "終了するまでアプリを閉じないでください。" "招待を準備中…" "ユーザーを招待" - "招待" "会話を退出" "ルームを退出" "ファイルとメディア" @@ -129,8 +128,6 @@ "役割" "ルームの詳細" "役割と権限" - "既読にする" - "未読にする" "アドレスを追加" "認証済みのスペースに所属するユーザーのみが参加できます。それ以外のユーザーは参加へのリクエストが必要です。" "参加のリクエストが必須です。" 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 8fd0b2c8a6..3ffc962603 100644 --- a/features/roomdetails/impl/src/main/res/values-ka/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ka/translations.xml @@ -39,7 +39,6 @@ "ამ ოთახის დადუმება ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა." "ამ ოთახის დადუმების მოხსნა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა." "ხალხის მოწვევა" - "მოწვევა" "საუბრის დატოვება" "ოთახის დატოვება" "მორგებული" @@ -94,6 +93,4 @@ "როლები" "ოთახის დეტალები" "როლები და ნებართვები" - "წაკითხულად მონიშვნა" - "წაუკითხავად მონიშვნა" 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 4a32cab45d..2c6462c4a7 100644 --- a/features/roomdetails/impl/src/main/res/values-ko/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ko/translations.xml @@ -56,7 +56,6 @@ "작업이 완료될 때까지 앱을 닫지 마세요." "초대 준비중…" "사람 초대하기" - "초대" "대화에서 나가기" "방 떠나기" "미디어 및 파일" @@ -129,8 +128,6 @@ "역할" "방 세부 정보" "역할 및 권한" - "읽음으로 표시" - "읽지 않음으로 표시" "주소 추가" "승인된 스페이스의 멤버는 누구나 참여할 수 있지만, 그 외의 인원은 액세스 요청을 해야 합니다." "모든 사용자가 액세스 권한을 요청해야 합니다." 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 9bdaeb86e5..84f74042da 100644 --- a/features/roomdetails/impl/src/main/res/values-lt/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-lt/translations.xml @@ -8,7 +8,6 @@ "Ž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 0cb3dfc268..e522f5cd79 100644 --- a/features/roomdetails/impl/src/main/res/values-nb/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-nb/translations.xml @@ -56,7 +56,6 @@ "Ikke lukk appen før den er ferdig." "Forbereder invitasjoner…" "Inviter folk" - "Inviter" "Forlat samtalen" "Forlat rommet" "Medier og filer" @@ -132,8 +131,6 @@ "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 2ee5caca97..9234921c35 100644 --- a/features/roomdetails/impl/src/main/res/values-nl/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-nl/translations.xml @@ -43,7 +43,6 @@ "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" @@ -100,6 +99,4 @@ "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 2979fcacdf..e22636290b 100644 --- a/features/roomdetails/impl/src/main/res/values-pl/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-pl/translations.xml @@ -1,24 +1,19 @@ - "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" + "Aby pokój był widoczny w katalogu, potrzebny jest adres pokoju." + "Adres pokoju" "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" - "Administrator" + "Tylko administratorzy" "Banowanie osób" "Usuń wiadomości" - "Członek" - "Zaproś osoby" - "Zarządzaj członkami" + "Zapraszanie osób i akceptowanie próśb o dołączenie" "Wiadomości i zawartość" - "Moderator" - "Usuń osoby" + "Administratorzy i moderatorzy" + "Usuwanie osób i odrzucanie próśb o dołączenie" "Zmień awatar pokoju" - "Edytuj szczegóły" + "Edytuj pokój" "Zmień nazwę pokoju" "Zmień temat pokoju" "Wysyłanie wiadomości" @@ -31,7 +26,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ące)" + "(Oczekujący)" "Administratorzy automatycznie mają uprawnienia moderatora" "Właściciele automatycznie mają uprawnienia administratora." "Edytuj moderatorów" @@ -45,7 +40,7 @@ "Szyfrowany" "Nieszyfrowany" "Pokój publiczny" - "Edytuj szczegóły" + "Edytuj pokój" "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." @@ -56,7 +51,6 @@ "Nie zamykaj aplikacji przed zakończeniem." "Przygotowywanie zaproszeń…" "Zaproś znajomych" - "Zaproś" "Opuść rozmowę" "Opuść pokój" "Media i pliki" @@ -67,21 +61,13 @@ "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…" - "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”" + "W tym pokoju nie ma zbanowanych użytkowników." "%1$d osoba" "%1$d osoby" @@ -94,22 +80,16 @@ "Odbanuj z pokoju" "Zbanowanych" "Członków" - - "%1$d zaproszony" - "%1$d zaproszonych" - "%1$d zaproszonych" - - "Oczekuje" - "Administrator" - "Moderator" + "Tylko administratorzy" + "Administratorzy i moderatorzy" "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 %1$s." - "ustawieniach globalnych" + "Możesz to zmienić w swoim %1$s." + "ustawienia globalne" "Ustawienie domyślne" "Usuń ustawienia własne" "Wystąpił błąd podczas ładowania ustawień powiadomień." @@ -128,20 +108,15 @@ "Wiadomości i zawartość" "Moderatorzy" "Właściciele" - "Uprawnienia" - "Zresetuj uprawnienia" + "Resetuj uprawnienia" "Po zresetowaniu uprawnień utracisz bieżące ustawienia." "Zresetować uprawnienia?" "Role" "Szczegóły pokoju" "Role i uprawnienia" - "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." + "Dodaj adres pokoju" + "Każdy może poprosić o dołączenie do pokoju, ale administrator lub moderator będzie musiał zatwierdzić żądanie." "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. @@ -150,31 +125,23 @@ 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 dołączyć." + "Każdy może znaleźć i dołączyć" "Każdy" - "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" + "Tylko osoby z zaproszeniem mogą dołączyć" + "Tylko zaproszenie" + "Dostęp do pokoju" "Przestrzenie nie są obecnie wspierane" - "Aby pokój był widoczny w katalogu pokoi publicznych, potrzebny jest adres pokoju." - "Adres" + "Aby pokój był widoczny w katalogu, potrzebny jest adres pokoju." + "Adres pokoju" "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" - "Każdy (historia jest publiczna)" - "Zmiany nie zmienią przeszłych wiadomości, tylko nowe. %1$s" + "Ktokolwiek" "Kto może czytać historię" - "Członkowie od kiedy zostali zaproszeni" - "Członkowie (cała historia)" + "Od momentu kiedy członkowie zostali zaproszeni" + "Członkowie od momentu włączenia tej opcji" "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" - "Adresy pokoi pomagają w znalezieniu i dołączeniu do pokoi i przestrzeni. Umożliwiają również łatwe udostępnianie ich innym." - "Widoczność" + "Widoczność pokoju" "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 f2ddfb86cb..ddbbe0f601 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,7 +53,6 @@ "Não feche o aplicativo até terminar." "Preparando convites…" "Convidar pessoas" - "Convidar" "Sair da conversa" "Sair da sala" "Mídia e arquivos" @@ -129,8 +128,6 @@ "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 04ded84480..592cba34f6 100644 --- a/features/roomdetails/impl/src/main/res/values-pt/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-pt/translations.xml @@ -53,7 +53,6 @@ "Não feches a aplicação até concluir." "A preparar convites…" "Convidar pessoas" - "Convidar" "Sair da conversa" "Sair da sala" "Multimédia e ficheiros" @@ -128,8 +127,6 @@ "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 f3feda811a..2eac873575 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -1,8 +1,5 @@ - "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." @@ -56,7 +53,6 @@ "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" @@ -135,12 +131,9 @@ "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. @@ -151,26 +144,22 @@ 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." - "Oricine" - "Alegeți membrii căror spații se pot alătura acestei cameră fără invitație. %1$s" + "Alegeți membrii căror spații se pot alătura acestei camere 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" - "Membri de la momentul invitației" - "Membri (istoric complet)" + "Doar pentru membri, de la momentul în care au fost invitați" + "Doar pentru membri, după selectarea acestei opțiuni" "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 bb36b3b23b..7ad44fff9d 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -56,7 +56,6 @@ "Не закрывайте приложение, пока не закончите." "Подготовка приглашений…" "Пригласить в комнату" - "Пригласить" "Покинуть беседу" "Покинуть комнату" "Медиа и файлы" @@ -135,8 +134,6 @@ "Роли" "Информация о комнате" "Роли и разрешения" - "Пометить как прочитанное" - "Отметить как непрочитанное" "Добавить адрес" "Кто угодно из авторизованных пространств может присоединиться, а всем остальным необходимо запросить доступ." "Каждый должен запросить доступ." 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 122c27e058..225dcedac3 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -53,7 +53,6 @@ "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" @@ -132,8 +131,6 @@ "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 aa9f244498..795dc97106 100644 --- a/features/roomdetails/impl/src/main/res/values-sv/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sv/translations.xml @@ -53,7 +53,6 @@ "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" @@ -116,8 +115,6 @@ "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 a9bfc6fbe2..16137ce753 100644 --- a/features/roomdetails/impl/src/main/res/values-tr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-tr/translations.xml @@ -46,7 +46,6 @@ "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" @@ -113,8 +112,6 @@ "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 8f566bb235..46adfa54d5 100644 --- a/features/roomdetails/impl/src/main/res/values-uk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-uk/translations.xml @@ -1,8 +1,5 @@ - "Нові учасники не бачать історії" - "Нові учасники бачать історію" - "Будь-хто може переглянути історію" "Вам знадобиться адреса кімнати, щоб зробити її видимою в каталозі." "Змінити адресу" "Під час оновлення налаштувань сповіщень сталася помилка." @@ -56,7 +53,6 @@ "Не закривайте застосунок доки не завершите." "Приготування запрошень…" "Запросити людей" - "Запросити" "Залишити розмову" "Вийти з кімнати" "Медіа та файли" @@ -75,11 +71,6 @@ "Тема" "Оновлення кімнати…" "Немає заблокованих користувачів." - - "%1$d Заблокований" - "%1$d Заблоковано" - "%1$d Заблоковано" - "Перевірте правопис або спробуйте новий пошук" "Немає результатів за запитом «%1$s»" @@ -135,13 +126,9 @@ "Ролі" "Деталі кімнати" "Ролі та дозволи" - "Позначити прочитаним" - "Позначити непрочитаним" "Додати адресу" - "Будь-хто в авторизованих просторах може приєднатися, але всі інші повинні подати запит на доступ." "Усі повинні запитувати доступ." "Запит на приєднання" - "Будь-хто з %1$s може приєднатися, але всі інші повинні подати запит на доступ." "Так, увімкнути шифрування" "Після ввімкнення шифрування кімнати, його неможливо вимкнути, історію повідомлень бачитимуть лише учасники кімнати, яких було запрошено або які приєдналися до кімнати. Ніхто, крім учасників кімнати, не зможе прочитати повідомлення. Це може перешкоджати коректній роботі ботів і мостів. @@ -157,14 +144,11 @@ "Приєднатися можуть лише запрошені люди." "Лише запрошені" "Доступ" - "Долучитися може будь-хто, хто має доступ до авторизованих просторів." "Долучитися може будь-хто з %1$s." - "Учасники простору" "Простори наразі не підтримуються" "Вам знадобиться адреса кімнати, щоб зробити її видимою в каталозі." "Адреса" "Дозвольте, щоб цю кімнату можна було знайти за допомогою пошуку в каталозі загальнодоступних кімнат %1$s " - "Дозвольте знаходити вас за допомогою пошуку в публічному каталозі." "Видима в загальному каталозі" "Будь-хто (загальнодоступна історія)" "Зміни не вплинуть на попередні повідомлення, лише на нові. %1$s" @@ -174,7 +158,6 @@ "Адреси кімнат — це спосіб знайти кімнату та отримати до неї доступ. Це також гарантує, що ви можете легко поділитися своєю кімнатою з іншими. Ви можете опублікувати свою кімнату в каталозі загальнодоступних кімнат вашого домашнього сервера." "Публікація в кімнаті" - "Адреси — це спосіб знаходити кімнати та простори та отримувати до них доступ. Це також гарантує, що ви зможете легко ділитися ними з іншими." "Видимість" "Безпека й приватність" 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 5c32811f36..3715bb91ff 100644 --- a/features/roomdetails/impl/src/main/res/values-ur/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ur/translations.xml @@ -43,7 +43,6 @@ "اس کمرے کو خاموش کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔" "اس کمرے کو غیر خاموش کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔" "لوگوں کو مدعو کریں" - "مدعو کریں" "گفتگو چھوڑیں" "کمرہ چھوڑ دیں" "حسب ضرورت" @@ -99,6 +98,4 @@ "کردارہا" "کمرے کی تفصیلات" "کردارہا اور اجازتیں" - "بطور مقروءہ نشانزد کریں" - "بطور غیر مقروءہ نشانزد کریں" 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 d7778f2df4..cac162cf2b 100644 --- a/features/roomdetails/impl/src/main/res/values-uz/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-uz/translations.xml @@ -1,8 +1,5 @@ - "Yangi a’zolar tarixni ko‘rmaydi" - "Yangi a’zolar tarixni ko‘radi" - "Tarixni hamma ko‘rishi mumkin" "Katalogda ko‘rinadigan qilish uchun xona manzili kerak bo‘ladi." "Xona manzili" "Bildirishnoma sozlamalarini yangilashda xatolik yuz berdi." @@ -12,7 +9,7 @@ "Odamlarni taqiqlash" "Xabarlarni olib tashlash" "A\'zo" - "Odamlarni taklif qiling" + "Odamlarni taklif qiling va qo‘shilish so‘rovlarini qabul qiling" "A’zolarni boshqarish" "Xabarlar va kontent" "Moderator" @@ -56,7 +53,6 @@ "Tugallanmaguncha ilovani yopmang." "Taklifnomalar tayyorlanmoqda…" "Odamlarni taklif qiling" - "Taklif qilish" "Suhbatni tark etish" "Xonani tark etish" "Media va fayllar" @@ -132,12 +128,9 @@ "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" - "Qo‘shilish uchun so‘rash" "%1$s ichidagi istalgan kishi qo‘shilishi mumkin, lekin qolganlar ruxsat so‘rashi kerak." "Ha, shifrlashni yoqish" "Yoqilgandan so‘ng, xona uchun shifrlashni o‘chirib bo‘lmaydi. Xabarlar tarixi faqat xona a’zolari taklif qilinganidan yoki xonaga qo‘shilganidan keyingi davrdan boshlab ko‘rinadi. Xona a’zolaridan tashqari hech kim xabarlarni o‘qiy olmaydi. Bu botlar va ko‘priklarning to‘g‘ri ishlashiga to‘sqinlik qilishi mumkin. @@ -147,7 +140,6 @@ Shu sababli, har kim topishi va qo‘shilishi mumkin bo‘lgan xonalar uchun shi "Shifrlash" "End-to-end shifrlashni yoqish" "Istalgan kishi topishi va qo‘shilishi mumkin" - "Har kim" "Qaysi maydonlar a’zolari bu xonaga taklifnomalarsiz kirishi mumkinligini tanlang. %1$s" "Maydonlarni boshqarish" "Odamlar faqat taklif qilingan taqdirdagina qo‘shilishi mumkin" @@ -155,18 +147,15 @@ Shu sababli, har kim topishi va qo‘shilishi mumkin bo‘lgan xonalar uchun shi "Xonaga kirish huquqi" "Ruxsat berilgan maydonlardagi istalgan kishi qo‘shilishi mumkin." "%1$s ichidagi istalgan kishi qo‘shilishi mumkin." - "Maydon a’zolari" "Hozirda maydonlar qo‘llab-quvvatlanmaydi" "Katalogda ko‘rinadigan qilish uchun xona manzili kerak bo‘ladi." "Manzil" "Bu xonani %1$s umumiy xonalar ro‘yxatidan qidirib topish imkoniyatini berish" "Umumiy katalogni qidirish orqali topishga ruxsat bering." "Umumiy xona ro‘yxatida ko‘rinadi" - "Har kim (tarix hammaga ochiq)" - "O‘zgarishlar avvalgi xabarlarga ta’sir qilmaydi, faqat yangilariga ta’sir qiladi.%1$s" "Tarixni kim o‘qiy oladi" - "Taklif qilinganidan beri a’zo" - "A’zolar (to‘liq tarix)" + "Taklif qilinganidan buyon faqat a’zolar" + "A’zolar faqat bu parametr tanlanganidan keyin" "Xona manzillari xonalarni topish va ularga kirish usullaridir. Bu shuningdek xonangizni boshqalar bilan oson ulashish imkonini beradi. Xonangizni o‘z homeserveringizning ommaviy xonalar ro‘yxatida e’lon qilishni tanlashingiz mumkin." "xona nashriyoti" 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 ecc7e4e227..a2ee35b563 100644 --- a/features/roomdetails/impl/src/main/res/values-vi/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-vi/translations.xml @@ -36,7 +36,6 @@ "Bạn có thay đổi chưa được lưu." "Lưu thay đổi?" "Thêm chủ đề" - "Phòng công cộng" "Chỉnh sửa thông tin" "Có lỗi không xác định, thông tin không được cập nhật." "Không thể cập nhật phòng" @@ -46,7 +45,6 @@ "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" @@ -60,9 +58,6 @@ "Chủ đề" "Đang cập nhật thông tin…" "Hiện không có người dùng nào bị cấm." - - "%1$d bị cấm" - "%1$d người" @@ -72,9 +67,6 @@ "Họ có thể tham gia lại phòng này nếu được mời." "Bị cấm" "Thành viên" - - "%1$d được mời" - "Quản trị viên" "Người điều hành" "Thành viên phòng" @@ -106,8 +98,6 @@ "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 0e516d0c29..2b784b91ea 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 @@ -1,8 +1,5 @@ - "新成員無法檢視歷史" - "新成員可以檢視歷史" - "任何人都可以檢視歷史" "您需要地址才能在公開目錄中顯示。" "編輯地址" "更新通知設定時發生錯誤。" @@ -56,7 +53,6 @@ "完成前請勿關閉應用程式。" "正在準備邀請……" "邀請夥伴" - "邀請" "離開對話" "離開聊天室" "媒體與檔案" @@ -67,7 +63,6 @@ "個人檔案" "請求加入" "角色與權限" - "名稱" "安全與隱私" "安全性" "分享聊天室" @@ -75,9 +70,6 @@ "主題" "正在更新聊天室…" "沒有被封鎖的使用者。" - - "%1$d 個已封鎖" - "檢查拼字或嘗試新搜尋" "找不到「%1$s」" @@ -90,9 +82,6 @@ "從聊天室解除封鎖" "黑名單" "成員" - - "%1$d 個已邀請" - "擱置中" "管理員" "版主" @@ -129,13 +118,9 @@ "身份" "聊天室資訊" "角色與權限" - "標為已讀" - "標為未讀" "新增地址" - "任何在授權空間的人都可以加入,但其他人都必須提出申請。" "所有人都必須申請存取權。" "要求加入" - "任何在 %1$s 中的人都可以加入,但其他人都必須提出申請。" "是的,啟用加密" "啟用後就無法停用聊天室的加密,只有受邀的聊天室成員或加入聊天室後才能看到訊息歷史紀錄。 除了聊天室成員以外,任何人都不能讀取訊息。這可能會讓機器人與橋接無法正常運作。 @@ -146,29 +131,22 @@ "啟用端到端加密" "任何人都可以加入。" "任何人" - "選擇哪些空間的成員不需要邀請就可以加入此聊天室。%1$s" - "管理空間" "僅受邀者才能加入。" "僅限邀請" "存取權" - "任何位於已授權空間的人都可以加入。" - "任何在 %1$s 中的人都可以加入。" - "空間成員" "目前不支援空間" "您需要地址才能在公開目錄中顯示。" "地址" "允許透過搜尋 %1$s 公開聊天室目錄找到此聊天室" "允許其他人透過公開目錄找到。" "在公開目錄中可見" - "任何人(歷史紀錄公開)" - "變更不會影響先前的訊息,只會影響新訊息。%1$s" + "任何人" "誰可以讀取歷史紀錄" - "成員,邀請後" - "成員(完整歷史)" + "僅在成員被邀請後" + "選取此選項後僅限成員" "聊天室地址是尋找與存取聊天室的方法。也確保您可以輕鬆與其他人分享聊天室。 您可以選擇在家伺服器公開聊天室目錄中發佈您的聊天室。" "聊天室發佈" - "地址是尋找與存取聊天室與空間的一種方式。這也讓您可以輕鬆地與其他人分享這些資訊。" "能見度" "安全與隱私" 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 8a5c5734ed..d803f01fb7 100644 --- a/features/roomdetails/impl/src/main/res/values-zh/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh/translations.xml @@ -1,15 +1,15 @@ - "新成员不能看到历史" - "新成员可以看到历史" + "新成员无法查看历史记录" + "新成员可见历史记录" "任何人都能查看历史记录" - "你需要一个地址才能使其在公共目录中可见。" + "您需要一个地址才能在公共目录中显示。" "编辑地址" "更新通知设置时出错。" - "主服务器不支持在加密房间中的此选项,因此在某些房间你可能无法收到通知。" + "服务器在加密聊天室中不支持此选项,因此在某些聊天室可能无法收到通知。" "投票" "管理员" - "封禁人员" + "封禁成员" "移除消息" "成员" "邀请人员" @@ -17,18 +17,18 @@ "消息和内容" "协管员" "移除人员" - "更改房间头像" + "更改聊天室头像" "编辑详情" - "更改房间名称" - "更改房间主题" + "更改聊天室名称" + "更改聊天室主题" "发送消息" "编辑管理员" - "此操作无法撤消。你正在提升用户的权限到与你相同的权力值。" + "您将无法撤消此操作。您正在提升用户的权限到与您相同的级别。" "添加管理员?" - "此操作无法撤消。你正在将所有权转移给所选用户。一旦离开此处,该操作将永久生效。" + "此操作无法撤销。您正在将所有权转移给所选用户。一旦离开此界面,该操作将永久生效。" "转让所有权" "降级" - "你正在降级自身,此更改无法撤消。如果你是房间中的最后一个拥有特权的用户,则无法重新获得权限。" + "您正在降级,此更改将无法撤消。如果您是聊天室中的最后一个特权用户,则无法重新获得权限。" "降级自己?" "%1$s(待处理)" "(待处理)" @@ -39,54 +39,53 @@ "管理员" "协管员" "成员" - "你有未保存的更改。" + "您有未保存的更改。" "保存更改?" "添加主题" "已加密" "未加密" - "公共房间" + "公共聊天室" "编辑详情" "出现未知错误,无法更改信息。" - "无法更新房间" + "无法更新聊天室" "消息已加密,只有你和消息接收者拥有唯一解密密钥。" "消息加密已启用" "加载通知设置时出错。" - "无法静音此房间,请重试。" - "无法取消静音此房间,请重试。" - "完成之前请勿关闭 app。" - "正在准备邀请…" - "邀请人员" - "邀请" + "无法将此聊天室静音,请重试。" + "无法取消此聊天室的静音,请重试。" + "完成之前请勿关闭应用程序。" + "准备邀请…" + "邀请朋友" "离开聊天" - "离开房间" - "媒体与文件" + "离开聊天室" + "媒体和文件" "自定义" "默认" "通知" - "已置顶的消息" + "置顶消息" "个人资料" "申请加入" "角色与权限" "名称" "安全与隐私" "安全" - "分享房间" - "房间信息" + "分享聊天室" + "聊天室信息" "主题" - "正在更新房间…" - "暂无被封禁的用户。" + "正在更新聊天室……" + "没有被封禁的用户。" - "%1$d 人被封禁" + "%1$d 被禁用" "检查拼写或尝试新搜索" "未找到 “%1$s” 相关结果" "%1$d 个人" - "封禁用户" + "移除并封禁成员" "仅移除成员" - "解封" - "如果他们受到邀请,则可以重新加入房间。" + "取消封禁" + "如果受到邀请,他们可以重新加入聊天室。" "解封用户" "已封禁用户" "成员" @@ -97,24 +96,24 @@ "管理员" "协管员" "所有者" - "房间成员" + "聊天室成员" "正在解除封禁 %1$s" "允许自定义设置" - "启用此功能将覆盖默认设置" + "开启此功能将覆盖您的默认设置" "在此聊天中通知我以下内容" - "你可以在 %1$s 中更改此项。" + "你可以在你的 %1$s 中更改这一项。" "全局设置" "默认设置" "撤销独立设置" "加载通知设置时出错。" "恢复默认模式失败,请重试。" "设置模式失败,请重试。" - "主服务器不支持在加密房间中的此选项,因此在此房间你可能无法收到通知。" - "所有消息" + "服务器在加密聊天室中不支持此选项,无法在此聊天室收到通知。" + "全部消息" "仅限提及和关键词" - "在此房间通知我以下类型" + "在这个聊天室,通知我:" "管理员" - "管理员与所有者" + "管理员和所有者" "更改我的角色" "降级为成员" "降级为协管员" @@ -124,21 +123,19 @@ "所有者" "权限" "重置权限" - "重置权限后你将丢失当前设置。" + "重置权限后,您将丢失当前设置。" "重置权限?" "角色" - "房间详细信息" + "聊天室详情" "角色与权限" - "设为已读" - "设为未读" "添加地址" - "已授权空间内的任何成员都可以加入,其他人必须申请访问。" + "授权空间内任何成员均可加入,其他人员需申请访问权限。" "所有用户均需申请访问权限。" - "申请加入" - "%1$s 成员可以加入,但其他人员必须申请访问。" - "是,启用加密" - "一旦启用,就不能再禁用房间的加密功能。消息历史只能在房间成员被邀请或加入房间后才可见。 -除房间成员外,任何人都无法阅读消息。这可能会阻止机器人和桥接器正常工作。 + "请求加入" + "%1$s 成员可自由加入,其他人员需申请访问权限。" + "是的,启用加密" + "一旦启用,就不能再禁用房间的加密功能。消息历史记录只能在房间成员被邀请或加入房间后才可见。 +除房间成员外,任何人都无法阅读信息。这可能会妨碍机器人和网桥正常工作。 我们不建议对任何人都能找到并加入的房间启用加密。" "启用加密?" "加密一旦启用,就无法禁用。" @@ -146,16 +143,16 @@ "启用端到端加密" "任何人都可以加入。" "任何人" - "选择哪些无需邀请即可加入此房间的空间成员。%1$s" + "选择哪些空间的成员无需邀请即可加入本聊天室。%1$s" "管理空间" - "仅限受邀人员加入。" + "仅限受邀者加入。" "仅限受邀者" "访问权限" "任何位于已授权空间的成员均可加入。" "%1$s 中的任何人都可加入。" "空间成员" - "“空间”功能当前不受支持" - "你需要一个地址才能使其在公共目录中可见。" + "目前不支持空间" + "您需要一个地址才能在公共目录中显示。" "地址" "允许通过搜索 %1$s 的公共房间目录来发现此房间" "通过公共目录搜索功能实现可被发现性。" @@ -163,12 +160,12 @@ "任何人(历史记录公开)" "更改不会影响之前的消息,只会影响新消息。%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 0287b5657b..6275b2837e 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -56,7 +56,6 @@ "Don\'t close the app until finished." "Preparing invitations…" "Invite people" - "Invite" "Leave conversation" "Leave room" "Media and files" @@ -132,8 +131,6 @@ "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 9a97380d21..bcf25b2aac 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt @@ -68,10 +68,9 @@ 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, clearBackStack: Boolean) = lambdaError() + override fun navigateToRoom(roomId: RoomId, serverNames: List) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() } diff --git a/features/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 deleted file mode 100644 index b416902dab..0000000000 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/FakeRoomDetailsNavigator.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.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 edb9115a7e..2d85744345 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,7 +15,6 @@ 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 @@ -29,7 +28,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 aFakeBaseRoom( +fun aRoom( sessionId: SessionId = A_SESSION_ID, roomId: RoomId = A_ROOM_ID, displayName: String = A_ROOM_NAME, @@ -50,7 +49,6 @@ fun aFakeBaseRoom( getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, userRoleResult: () -> Result = { lambdaError() }, setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, - markAsReadResult: (ReceiptType) -> Result = { lambdaError() }, ) = FakeBaseRoom( sessionId = sessionId, roomId = roomId, @@ -59,7 +57,6 @@ fun aFakeBaseRoom( getUpdatedMemberResult = getUpdatedMemberResult, userRoleResult = userRoleResult, setIsFavoriteResult = setIsFavoriteResult, - markAsReadResult = markAsReadResult, roomPermissions = roomPermissions, initialRoomInfo = aRoomInfo( name = displayName, @@ -109,7 +106,6 @@ fun aJoinedRoom( publishRoomAliasInRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, removeRoomAliasFromRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, - markAsReadResult: (ReceiptType) -> Result = { lambdaError() }, ) = FakeJoinedRoom( roomNotificationSettingsService = notificationSettingsService, setNameResult = setNameResult, @@ -122,7 +118,7 @@ fun aJoinedRoom( updateCanonicalAliasResult = updateCanonicalAliasResult, publishRoomAliasInRoomDirectoryResult = publishRoomAliasInRoomDirectoryResult, removeRoomAliasFromRoomDirectoryResult = removeRoomAliasFromRoomDirectoryResult, - baseRoom = aFakeBaseRoom( + baseRoom = aRoom( sessionId = sessionId, roomId = roomId, roomPermissions = roomPermissions, @@ -143,6 +139,5 @@ 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 51a6f97ab0..d355010307 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,8 +21,9 @@ 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.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.SessionId +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.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembersState @@ -30,7 +31,6 @@ 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,11 +44,7 @@ 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 @@ -84,12 +80,14 @@ 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(), - navigator: RoomDetailsNavigator = FakeRoomDetailsNavigator(), - notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), - sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore() ): RoomDetailsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { @@ -106,9 +104,9 @@ class RoomDetailsPresenterTest { } } return RoomDetailsPresenter( - navigator = navigator, client = matrixClient, room = room, + featureFlagService = featureFlagService, notificationSettingsService = matrixClient.notificationSettingsService, roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory, leaveRoomPresenter = { leaveRoomState }, @@ -117,8 +115,6 @@ class RoomDetailsPresenterTest { analyticsService = analyticsService, clipboardHelper = clipboardHelper, appPreferencesStore = appPreferencesStore, - notificationCleaner = notificationCleaner, - sessionPreferencesStore = sessionPreferencesStore, ) } @@ -203,14 +199,19 @@ class RoomDetailsPresenterTest { givenRoomInfo( aRoomInfo( isEncrypted = true, - isDm = true, + isDirect = true, ) ) } val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { val initialState = awaitItem() - assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherMember = otherRoomMember)) + assertThat(initialState.roomType).isEqualTo( + RoomDetailsType.Dm( + me = myRoomMember, + otherMember = otherRoomMember, + ) + ) cancelAndIgnoreRemainingEvents() } } @@ -288,7 +289,7 @@ class RoomDetailsPresenterTest { givenRoomInfo( aRoomInfo( isEncrypted = true, - isDm = true, + isDirect = true, ) ) } @@ -311,6 +312,7 @@ 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) }, @@ -328,7 +330,7 @@ class RoomDetailsPresenterTest { givenRoomInfo( aRoomInfo( - isDm = true, + isDirect = true, activeMembersCount = 2, topic = null, ) @@ -568,11 +570,17 @@ 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() @@ -585,7 +593,8 @@ class RoomDetailsPresenterTest { val room = aJoinedRoom( roomPermissions = roomPermissions(), ) - val presenter = createRoomDetailsPresenter(room = room) + val featureFlagService = FakeFeatureFlagService() + val presenter = createRoomDetailsPresenter(room = room, featureFlagService = featureFlagService) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { skipItems(1) with(awaitItem()) { @@ -611,87 +620,6 @@ 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/RoomDetailsStateTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateTest.kt index 54ad539bfb..7bfd52d82d 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateTest.kt @@ -37,24 +37,24 @@ class RoomDetailsStateTest { } @Test - fun `room public encrypted should have encrypted, public, and history sharing shared badges`() { + fun `room public encrypted should have encrypted and public badges`() { val sut = aRoomDetailsState( isPublic = true, isEncrypted = true, ) assertThat(sut.roomBadges).isEqualTo( - persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.PUBLIC, RoomBadge.SHARED_HISTORY_SHARED) + persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.PUBLIC) ) } @Test - fun `room not public encrypted should have encrypted and history sharing shared badges`() { + fun `room not public encrypted should have encrypted badges`() { val sut = aRoomDetailsState( isPublic = false, isEncrypted = true, ) assertThat(sut.roomBadges).isEqualTo( - persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.SHARED_HISTORY_SHARED) + persistentListOf(RoomBadge.ENCRYPTED) ) } @@ -62,6 +62,7 @@ class RoomDetailsStateTest { fun `room public not encrypted should not have history sharing badges`() { val sut = aRoomDetailsState( isEncrypted = false, + enableKeyShareOnInvite = true, roomHistoryVisibility = RoomHistoryVisibility.Shared ) assertThat(sut.roomBadges).isEqualTo( @@ -73,6 +74,7 @@ class RoomDetailsStateTest { fun `room public encrypted should have history sharing hidden badge`() { val sut = aRoomDetailsState( isEncrypted = true, + enableKeyShareOnInvite = true, roomHistoryVisibility = RoomHistoryVisibility.Joined ) assertThat(sut.roomBadges).isEqualTo( @@ -81,9 +83,22 @@ class RoomDetailsStateTest { } @Test - fun `room public encrypted with world_readable visibility should have history sharing world_readable badge`() { + fun `room public encrypted should have history sharing shared badge`() { val sut = aRoomDetailsState( isEncrypted = true, + enableKeyShareOnInvite = true, + roomHistoryVisibility = RoomHistoryVisibility.Shared + ) + assertThat(sut.roomBadges).isEqualTo( + persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.PUBLIC, RoomBadge.SHARED_HISTORY_SHARED) + ) + } + + @Test + fun `room public encrypted should have history sharing world_readable badge`() { + val sut = aRoomDetailsState( + isEncrypted = true, + enableKeyShareOnInvite = true, roomHistoryVisibility = RoomHistoryVisibility.WorldReadable ) assertThat(sut.roomBadges).isEqualTo( 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 9494df5d44..588a10a218 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,19 +6,14 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.onAllNodesWithText -import androidx.compose.ui.test.onLast +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.performClick -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.userprofile.shared.aUserProfileState @@ -37,332 +32,323 @@ 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 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() + @Test - fun `click on back invokes expected callback`() = runAndroidComposeUiTest { + fun `click on back invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( goBack = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `click on share invokes expected callback`() = runAndroidComposeUiTest { + fun `click on share invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( onShareRoom = callback, ) - clickOn(CommonStrings.action_share) + rule.clickOn(CommonStrings.action_share) } } @Config(qualifiers = "h1024dp") @Test - fun `click on room members invokes expected callback`() = runAndroidComposeUiTest { + fun `click on room members invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( openRoomMemberList = callback, ) - clickOn(CommonStrings.common_people) + rule.clickOn(CommonStrings.common_people) } } @Config(qualifiers = "h1024dp") @Test - fun `click on polls invokes expected callback`() = runAndroidComposeUiTest { + fun `click on polls invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( openPollHistory = callback, ) - clickOn(R.string.screen_polls_history_title) + rule.clickOn(R.string.screen_polls_history_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on media gallery invokes expected callback`() = runAndroidComposeUiTest { + fun `click on media gallery invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( openMediaGallery = callback, ) - clickOn(R.string.screen_room_details_media_gallery_title) + rule.clickOn(R.string.screen_room_details_media_gallery_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on notification invokes expected callback`() = runAndroidComposeUiTest { + fun `click on notification invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( openRoomNotificationSettings = callback, ) - clickOn(R.string.screen_room_details_notification_title) + rule.clickOn(R.string.screen_room_details_notification_title) } } @Test - fun `click on invite invokes expected callback`() = runAndroidComposeUiTest { + fun `click on invite invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, ), invitePeople = callback, ) - clickOn(CommonStrings.action_invite) + rule.clickOn(CommonStrings.action_invite) } } @Test - fun `click on call invokes expected callback`() = runAndroidComposeUiTest { + fun `click on call invokes expected callback`() { ensureCalledOnceWithParam(CallIntent.AUDIO) { callback -> - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, - roomType = RoomDetailsType.Dm(aRoomMember(UserId("@other:local.org"))), + roomType = RoomDetailsType.Dm( + aRoomMember(UserId("@me:local.org")), + aRoomMember(UserId("@other:local.org")) + ), ), onJoinCallClick = callback, ) - clickOn(CommonStrings.action_call) + rule.clickOn(CommonStrings.action_call) } } @Test - fun `click on video call invokes expected callback`() = runAndroidComposeUiTest { + fun `click on video call invokes expected callback`() { ensureCalledOnceWithParam(CallIntent.VIDEO) { callback -> - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, ), onJoinCallClick = callback, ) - clickOn(CommonStrings.common_video) + rule.clickOn(CommonStrings.common_video) } } @Config(qualifiers = "h1024dp") @Test - fun `click on pinned messages invokes expected callback`() = runAndroidComposeUiTest { + fun `click on pinned messages invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, ), onPinnedMessagesClick = callback, ) - clickOn(R.string.screen_room_details_pinned_events_row_title) + rule.clickOn(R.string.screen_room_details_pinned_events_row_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on security and privacy invokes expected callback`() = runAndroidComposeUiTest { + fun `click on security and privacy invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canShowSecurityAndPrivacy = true, ), onSecurityAndPrivacyClick = callback, ) - clickOn(R.string.screen_room_details_security_and_privacy_title) + rule.clickOn(R.string.screen_room_details_security_and_privacy_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on add topic emit expected event`() = runAndroidComposeUiTest { + fun `click on add topic emit expected event`() { ensureCalledOnceWithParam(RoomDetailsAction.AddTopic) { callback -> - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), roomTopic = RoomTopicState.CanAddTopic, ), onActionClick = callback, ) - clickOn(R.string.screen_room_details_add_topic_title) + rule.clickOn(R.string.screen_room_details_add_topic_title) } } @Test - fun `click on menu edit emit expected event`() = runAndroidComposeUiTest { + fun `click on menu edit emit expected event`() { ensureCalledOnceWithParam(RoomDetailsAction.Edit) { callback -> - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canEdit = true, ), onActionClick = callback, ) - val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) - onNodeWithContentDescription(menuContentDescription).performClick() - clickOn(CommonStrings.action_edit) + val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) + rule.onNodeWithContentDescription(menuContentDescription).performClick() + rule.clickOn(CommonStrings.action_edit) } } @Test - fun `click on avatar test`() = runAndroidComposeUiTest { + fun `click on avatar test`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aRoomDetailsState( eventSink = eventsRecorder, roomAvatarUrl = "an_avatar_url", ) val callback = EnsureCalledOnceWithTwoParams(state.roomName, "an_avatar_url") - setRoomDetailView( + rule.setRoomDetailView( state = state, openAvatarPreview = callback, ) - onNodeWithTag(TestTags.roomDetailAvatar.value).performClick() + rule.onNodeWithTag(TestTags.roomDetailAvatar.value).performClick() callback.assertSuccess() } @Test - fun `click on avatar test on DM`() = runAndroidComposeUiTest { + fun `click on avatar test on DM`() { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aRoomDetailsState( - roomType = RoomDetailsType.Dm(aDmRoomMember(avatarUrl = "an_avatar_url"),), + roomType = RoomDetailsType.Dm( + aRoomMember(), + aDmRoomMember(avatarUrl = "an_avatar_url"), + ), roomName = "Daniel", eventSink = eventsRecorder, ) val callback = EnsureCalledOnceWithTwoParams("Daniel", "an_avatar_url") - setRoomDetailView( + rule.setRoomDetailView( state = state, openAvatarPreview = callback, ) - onNodeWithTag(TestTags.roomDetailAvatar.value).performClick() + rule.onNodeWithTag(TestTags.memberDetailAvatar.value).performClick() callback.assertSuccess() } @Test - fun `click on mute emit expected event`() = runAndroidComposeUiTest { + fun `click on mute emit expected event`() { val eventsRecorder = EventsRecorder() val state = aRoomDetailsState( eventSink = eventsRecorder, roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES), ) - setRoomDetailView( + rule.setRoomDetailView( state = state, ) - clickOn(CommonStrings.common_mute) + rule.clickOn(CommonStrings.common_mute) eventsRecorder.assertSingle(RoomDetailsEvent.MuteNotification) } @Test - fun `click on unmute emit expected event`() = runAndroidComposeUiTest { + fun `click on unmute emit expected event`() { val eventsRecorder = EventsRecorder() val state = aRoomDetailsState( eventSink = eventsRecorder, roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.MUTE), ) - setRoomDetailView( + rule.setRoomDetailView( state = state, ) - clickOn(CommonStrings.common_unmute) + rule.clickOn(CommonStrings.common_unmute) eventsRecorder.assertSingle(RoomDetailsEvent.UnmuteNotification) } @Config(qualifiers = "h1024dp") @Test - fun `click on favorite emit expected Event`() = runAndroidComposeUiTest { + fun `click on favorite emit expected Event`() { val eventsRecorder = EventsRecorder() - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.common_favourite) + rule.clickOn(CommonStrings.common_favourite) eventsRecorder.assertSingle(RoomDetailsEvent.SetFavorite(true)) } @Config(qualifiers = "h1500dp") @Test - fun `click on leave emit expected Event`() = runAndroidComposeUiTest { + fun `click on leave emit expected Event`() { val eventsRecorder = EventsRecorder() - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_room_details_leave_room_title) + rule.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`() = runAndroidComposeUiTest { + fun `click on report room invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), ), onReportRoomClick = callback, ) - clickOn(CommonStrings.action_report_room) + rule.clickOn(CommonStrings.action_report_room) } } @Config(qualifiers = "h1024dp") @Test - fun `click on knock requests invokes expected callback`() = runAndroidComposeUiTest { + fun `click on knock requests invokes expected callback`() { ensureCalledOnce { callback -> - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canShowKnockRequests = true, ), onKnockRequestsClick = callback, ) - clickOn(R.string.screen_room_details_requests_to_join_title) + rule.clickOn(R.string.screen_room_details_requests_to_join_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on profile invokes the expected callback`() = runAndroidComposeUiTest { + fun `click on profile invokes the expected callback`() { ensureCalledOnceWithParam(A_USER_ID) { callback -> - setRoomDetailView( + rule.setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), roomMemberDetailsState = aUserProfileState(userId = A_USER_ID), ), onProfileClick = callback, ) - 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")), - ), - roomMemberDetailsState = aUserProfileState(userId = A_USER_ID), - canInvite = true, - ), - invitePeople = callback, - ) - onAllNodesWithText(activity!!.getString(R.string.screen_room_details_invite_title)).onLast().performClick() + rule.clickOn(R.string.screen_room_details_profile_row_title) } } } -private fun AndroidComposeUiTest.setRoomDetailView( +private fun AndroidComposeTestRule.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 deleted file mode 100644 index 8be6967ace..0000000000 --- a/features/roomdetailsedit/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - "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 a339ecd57f..bbbd63d171 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 25be496204..c676ff46ed 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 szczegóły" + "Edytuj pokój" "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/main/res/values-zh/translations.xml b/features/roomdetailsedit/impl/src/main/res/values-zh/translations.xml index ae6d0167c3..cf7abd7cc8 100644 --- a/features/roomdetailsedit/impl/src/main/res/values-zh/translations.xml +++ b/features/roomdetailsedit/impl/src/main/res/values-zh/translations.xml @@ -2,6 +2,6 @@ "编辑详情" "出现未知错误,无法更改信息。" - "无法更新房间" - "正在更新房间…" + "无法更新聊天室" + "正在更新聊天室……" 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 686794d641..71fb143074 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,21 +5,18 @@ * 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.media.AvatarAction @@ -31,54 +28,58 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack 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() + @Test - fun `clicking on back emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on back emits the expected Event`() { val eventsRecorder = EventsRecorder() - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder ), ) - pressBack() + rule.pressBack() eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress) } @Test - fun `clicking on discard when confirming exit emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on discard when confirming exit emits the expected Event`() { val eventsRecorder = EventsRecorder() - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_discard) + rule.clickOn(CommonStrings.action_discard) eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress) } @Test - fun `clicking on save when confirming exit emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on save when confirming exit emits the expected Event`() { val eventsRecorder = EventsRecorder() - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_save, inDialog = true) + rule.clickOn(CommonStrings.action_save, inDialog = true) eventsRecorder.assertSingle(RoomDetailsEditEvent.Save) } @Test - fun `when edition is successful, the expected callback is invoked`() = runAndroidComposeUiTest { + fun `when edition is successful, the expected callback is invoked`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveAction = AsyncAction.Success(Unit) @@ -89,55 +90,55 @@ class RoomDetailsEditViewTest { } @Test - fun `when name is changed, the expected Event is emitted`() = runAndroidComposeUiTest { + fun `when name is changed, the expected Event is emitted`() { val eventsRecorder = EventsRecorder() - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomRawName = "Marketing", ), ) - onNodeWithText("Marketing").performTextInput("A") + rule.onNodeWithText("Marketing").performTextInput("A") eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomName("AMarketing")) } @Test - fun `when user cannot change name, nothing happen`() = runAndroidComposeUiTest { + fun `when user cannot change name, nothing happen`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomRawName = "Marketing", canChangeName = false, ), ) - onNodeWithText("Marketing").assert(!isEditable()) + rule.onNodeWithText("Marketing").assert(!isEditable()) } @Test - fun `when topic is changed, the expected Event is emitted`() = runAndroidComposeUiTest { + fun `when topic is changed, the expected Event is emitted`() { val eventsRecorder = EventsRecorder() - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomTopic = "My Topic", ), ) - onNodeWithText("My Topic").performTextInput("A") + rule.onNodeWithText("My Topic").performTextInput("A") eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomTopic("AMy Topic")) } @Test - fun `when user cannot change topic, nothing happen`() = runAndroidComposeUiTest { + fun `when user cannot change topic, nothing happen`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomTopic = "My Topic", canChangeTopic = false, ), ) - onNodeWithText("My Topic").assert(!isEditable()) + rule.onNodeWithText("My Topic").assert(!isEditable()) } @Ignore("This test is failing because the bottom sheet does not open") @@ -170,73 +171,73 @@ class RoomDetailsEditViewTest { private fun testAvatarChange( @StringRes stringActionRes: Int, expectedEvent: RoomDetailsEditEvent.HandleAvatarAction, - ) = runAndroidComposeUiTest { + ) { val eventsRecorder = EventsRecorder() - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, ), ) // Open the bottom sheet - onNode(hasTestTag(TestTags.editAvatar.value)).performClick() - onNodeWithText(activity!!.getString(stringActionRes)).assertExists() - clickOn(stringActionRes) + rule.onNode(hasTestTag(TestTags.editAvatar.value)).performClick() + rule.onNodeWithText(rule.activity.getString(stringActionRes)).assertExists() + rule.clickOn(stringActionRes) eventsRecorder.assertSingle(expectedEvent) } @Test - fun `when user cannot change avatar, nothing happen`() = runAndroidComposeUiTest { + fun `when user cannot change avatar, nothing happen`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, canChangeAvatar = false, ), ) - onNode(hasTestTag(TestTags.editAvatar.value)).performClick() - onNodeWithText(activity!!.getString(CommonStrings.action_take_photo)).assertDoesNotExist() + rule.onNode(hasTestTag(TestTags.editAvatar.value)).performClick() + rule.onNodeWithText(rule.activity.getString(CommonStrings.action_take_photo)).assertDoesNotExist() } @Test - fun `when save is clicked, the expected Event is emitted`() = runAndroidComposeUiTest { + fun `when save is clicked, the expected Event is emitted`() { val eventsRecorder = EventsRecorder() - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveButtonEnabled = true, ), ) - clickOn(CommonStrings.action_save) + rule.clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(RoomDetailsEditEvent.Save) } @Test - fun `when save is clicked, but nothing need to be saved, nothing happens`() = runAndroidComposeUiTest { + fun `when save is clicked, but nothing need to be saved, nothing happens`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveButtonEnabled = false, ), ) - clickOn(CommonStrings.action_save) + rule.clickOn(CommonStrings.action_save) } @Test - fun `when error is shown, closing the dialog emit the expected Event`() = runAndroidComposeUiTest { + fun `when error is shown, closing the dialog emit the expected Event`() { val eventsRecorder = EventsRecorder() - setRoomDetailsEditView( + rule.setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveAction = AsyncAction.Failure(RuntimeException("Whelp")), ), ) - clickOn(CommonStrings.action_ok) + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(RoomDetailsEditEvent.CloseDialog) } } -private fun AndroidComposeUiTest.setRoomDetailsEditView( +private fun AndroidComposeTestRule.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 deleted file mode 100644 index fc664290c1..0000000000 --- a/features/roomdirectory/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "No s\'ha pogut carregar" - "Directori de sales" - diff --git a/features/roomdirectory/impl/src/main/res/values-zh/translations.xml b/features/roomdirectory/impl/src/main/res/values-zh/translations.xml index 705b8e353f..742a762858 100644 --- a/features/roomdirectory/impl/src/main/res/values-zh/translations.xml +++ b/features/roomdirectory/impl/src/main/res/values-zh/translations.xml @@ -1,5 +1,5 @@ "加载失败" - "房间目录" + "聊天室目录" 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 f9d60f87da..a50ad6a22c 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,18 +6,15 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.testtags.TestTags @@ -25,27 +22,31 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RoomDirectoryViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `typing text in search field emits the expected Event`() = runAndroidComposeUiTest { + fun `typing text in search field emits the expected Event`() { val eventsRecorder = EventsRecorder() - setRoomDirectoryView( + rule.setRoomDirectoryView( state = aRoomDirectoryState( eventSink = eventsRecorder, ) ) - onNodeWithTag(TestTags.searchTextField.value).performTextInput( + rule.onNodeWithTag(TestTags.searchTextField.value).performTextInput( text = "Test" ) eventsRecorder.assertSingle(RoomDirectoryEvents.Search("Test")) } @Test - fun `clicking on room item then onResultClick lambda is called once`() = runAndroidComposeUiTest { + fun `clicking on room item then onResultClick lambda is called once`() { val eventsRecorder = EventsRecorder() val state = aRoomDirectoryState( roomDescriptions = aRoomDescriptionList(), @@ -53,27 +54,27 @@ class RoomDirectoryViewTest { ) val clickedRoom = state.roomDescriptions.first() ensureCalledOnceWithParam(clickedRoom) { callback -> - setRoomDirectoryView( + rule.setRoomDirectoryView( state = state, onResultClick = callback, ) - onNodeWithText(clickedRoom.computedName).performClick() + rule.onNodeWithText(clickedRoom.computedName).performClick() } } @Test - fun `composing load more indicator emits expected Event`() = runAndroidComposeUiTest { + fun `composing load more indicator emits expected Event`() { val eventsRecorder = EventsRecorder() val state = aRoomDirectoryState( displayLoadMoreIndicator = true, eventSink = eventsRecorder, ) - setRoomDirectoryView(state = state) + rule.setRoomDirectoryView(state = state) eventsRecorder.assertSingle(RoomDirectoryEvents.LoadMore) } } -private fun AndroidComposeUiTest.setRoomDirectoryView( +private fun AndroidComposeTestRule.setRoomDirectoryView( state: RoomDirectoryState, onBackClick: () -> Unit = EnsureNeverCalled(), onResultClick: (RoomDescription) -> Unit = EnsureNeverCalledWithParam(), 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 2bb4db0c69..120a299a7d 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,7 +14,6 @@ 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 @@ -79,8 +78,8 @@ class InternalRoomMemberModerationStateProvider : PreviewParameterProvider - - "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 6ddb482189..f17660f418 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 40ea1f099a..20246af787 100644 --- a/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml @@ -4,14 +4,12 @@ "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ń użytkownika" + "Usuń z pokoju" "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 0ddc115f7d..cd6dd40e55 100644 --- a/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml @@ -4,12 +4,10 @@ "Заблокувати" "Він не зможе приєднатися до цієї кімнати знову, якщо його запросять." "Ви точно хочете заблокувати цього користувача?" - "Вони не зможуть знову приєднатися до цього простору, навіть якщо їх запросять, але збережуть своє членство в будь-яких кімнатах або підпросторах." "Блокування %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 bfd456f130..2c18ee4216 100644 --- a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml @@ -1,22 +1,22 @@ - "封禁用户" + "移除并封禁成员" "封禁" - "他们即使受到邀请也无法再次加入房间。" - "你确定要封禁该成员?" - "即使再次被邀请,他们也无法加入此空间,但其在任何房间或子空间的成员资格仍然保留。" + "即使受到邀请,他们也无法再次加入聊天室。" + "您确定要封禁该成员吗?" + "即使再次受邀,他们也无法加入这个空间,但他们仍将保留其在任何房间或子空间的成员资格。" "正在封禁 %1$s" "移除" - "如果他们受到邀请,则可以重新加入房间。" - "你确定要移除此成员?" - "如果被邀请,他们将能够再次加入此空间,并且其在任何房间或子空间的成员资格仍然保留。" + "如果受到邀请,他们可以重新加入聊天室。" + "您确定要移除此成员吗?" + "如果受到邀请,他们将能够再次加入这个空间,并且他们仍将保留其在任何房间或子空间的成员资格。" "查看个人资料" "移除用户" "删除成员并禁止重新加入?" - "正在移除 %1$s…" + "正在移除 %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 646481715a..6508b28053 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState @@ -27,17 +24,21 @@ 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.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() + @Test - fun `clicking on display profile action calls onSelectAction`() = runAndroidComposeUiTest { + fun `clicking on display profile action calls onSelectAction`() { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.DisplayProfile, user) { callback -> - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -47,16 +48,16 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info) } } @Test - fun `clicking on kick user action calls onSelectAction`() = runAndroidComposeUiTest { + fun `clicking on kick user action calls onSelectAction`() { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.KickUser, user) { callback -> - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -66,18 +67,18 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) // Gives time for bottomsheet to hide - mainClock.advanceTimeBy(1_000) + rule.mainClock.advanceTimeBy(1_000) } } @Test - fun `clicking on ban user action calls onSelectAction`() = runAndroidComposeUiTest { + fun `clicking on ban user action calls onSelectAction`() { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.BanUser, user) { callback -> - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -87,18 +88,18 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - clickOn(R.string.screen_bottom_sheet_manage_room_member_ban) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_ban) // Gives time for bottomsheet to hide - mainClock.advanceTimeBy(1_000) + rule.mainClock.advanceTimeBy(1_000) } } @Test - fun `clicking on unban user action calls onSelectAction`() = runAndroidComposeUiTest { + fun `clicking on unban user action calls onSelectAction`() { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.UnbanUser, user) { callback -> - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -108,100 +109,100 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - clickOn(R.string.screen_bottom_sheet_manage_room_member_unban) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_unban) // Gives time for bottomsheet to hide - mainClock.advanceTimeBy(1_000) + rule.mainClock.advanceTimeBy(1_000) } } @Test - fun `clicking submit on kick confirmation dialog sends DoKickUser event`() = runAndroidComposeUiTest { + fun `clicking submit on kick confirmation dialog sends DoKickUser event`() { val eventsRecorder = EventsRecorder() - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), kickUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - pressTag(TestTags.dialogPositive.value) + rule.pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoKickUser(reason = "")) } @Test - fun `clicking dismiss on kick confirmation dialog sends Reset event`() = runAndroidComposeUiTest { + fun `clicking dismiss on kick confirmation dialog sends Reset event`() { val eventsRecorder = EventsRecorder() - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), kickUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - pressTag(TestTags.dialogNegative.value) + rule.pressTag(TestTags.dialogNegative.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) } @Test - fun `clicking submit on ban confirmation dialog sends DoBanUser event`() = runAndroidComposeUiTest { + fun `clicking submit on ban confirmation dialog sends DoBanUser event`() { val eventsRecorder = EventsRecorder() - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), banUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - pressTag(TestTags.dialogPositive.value) + rule.pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoBanUser(reason = "")) } @Test - fun `clicking dismiss on ban confirmation dialog sends Reset event`() = runAndroidComposeUiTest { + fun `clicking dismiss on ban confirmation dialog sends Reset event`() { val eventsRecorder = EventsRecorder() - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), banUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - pressTag(TestTags.dialogNegative.value) + rule.pressTag(TestTags.dialogNegative.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) } @Test - fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() = runAndroidComposeUiTest { + fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() { val eventsRecorder = EventsRecorder() - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - pressTag(TestTags.dialogPositive.value) + rule.pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser("")) } @Test - fun `clicking dismiss on unban confirmation dialog sends Reset event`() = runAndroidComposeUiTest { + fun `clicking dismiss on unban confirmation dialog sends Reset event`() { val eventsRecorder = EventsRecorder() - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - pressTag(TestTags.dialogNegative.value) + rule.pressTag(TestTags.dialogNegative.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) } @Test - fun `disabled actions are not clickable`() = runAndroidComposeUiTest { + fun `disabled actions are not clickable`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setRoomMemberModerationView( + rule.setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), actions = listOf( @@ -210,11 +211,11 @@ class RoomMemberModerationViewTest { eventSink = eventsRecorder ), ) - clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) + rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) } } -private fun AndroidComposeUiTest.setRoomMemberModerationView( +private fun AndroidComposeTestRule.setRoomMemberModerationView( state: InternalRoomMemberModerationState, onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(), ) { diff --git a/features/securebackup/impl/build.gradle.kts b/features/securebackup/impl/build.gradle.kts index 54d87ef22e..b6117271f7 100644 --- a/features/securebackup/impl/build.gradle.kts +++ b/features/securebackup/impl/build.gradle.kts @@ -28,14 +28,13 @@ 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.oauth.api) + implementation(projects.libraries.oidc.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/reset/ResetIdentityFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt index 7dd6bf632a..c4a007f1d5 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,7 +25,6 @@ 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 @@ -37,7 +36,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.IdentityOAuthResetHandle +import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -54,7 +53,6 @@ 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, @@ -125,13 +123,12 @@ class ResetIdentityFlowNode( null -> { Timber.d("No reset handle return, the reset is done.") } - is IdentityOAuthResetHandle -> { + is IdentityOidcResetHandle -> { Timber.d("Launching reset confirmation in MAS") - 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") } + activity.openUrlInChromeCustomTab(null, darkTheme, handle.url) + Timber.d("Starting resetOidc") + resetJob = launch { handle.resetOidc() } + resetJob?.invokeOnCompletion { Timber.d("resetOidc ended") } } is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) } diff --git a/features/securebackup/impl/src/main/res/values-ca/translations.xml b/features/securebackup/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 0ca99788ed..0000000000 --- a/features/securebackup/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - "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 d53086a6d1..84140d4da0 100644 --- a/features/securebackup/impl/src/main/res/values-de/translations.xml +++ b/features/securebackup/impl/src/main/res/values-de/translations.xml @@ -2,16 +2,16 @@ "Backup deaktivieren" "Backup aktivieren" - "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." + "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." "Schlüsselspeicher" - "Die Schlüsselspeicherung muss aktiviert sein, damit deine Chats gesichert werden können." + "Der Schlüsselspeicher muss aktiviert sein, um Datenwiederherstellung zu ermöglichen." "Schlüssel von diesem Gerät hochladen" "Schlüsselspeicherung zulassen" "Wiederherstellungsschlüssel ändern" "Stelle deine kryptographische Identität und deinen Nachrichtenverlauf mit einem Wiederherstellungsschlüssel wieder her, falls du deine Geräte verloren hast." "Wiederherstellungsschlüssel eingeben" "Dein Schlüssel ist derzeit nicht synchronisiert." - "Wiederherstellungsschlüssel einrichten" + "Wiederherstellung einrichten" "Öffne " "%1$s" @@ -32,12 +32,12 @@ "Deine Kontodaten, Kontakte, Einstellungen und die Liste der Chats bleiben erhalten" "Du verlierst alle bisherigen Nachrichten, wenn sie ausschließlich auf dem Server gespeichert sein sollten." "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." - "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:" + "Setze deine Identität nur dann zurück, wenn du keinen Zugriff mehr auf ein anderes angemeldetes Gerät hast und auch deinen Wiederherstellungsschlüssel verloren hast." + "Bestätigung unmöglich? Dann musst du deine Identität zurücksetzen." + "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:" "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?" @@ -67,12 +67,12 @@ "Wiederherstellungsschlüssel erstellen" "Teile das mit niemandem!" "Einrichtung der Wiederherstellung erfolgreich" - "Wiederherstellungsschlüssel einrichten" + "Wiederherstellung einrichten" "Ja, zurücksetzen" "Das Zurücksetzen kann nicht rückgängig gemacht werden." - "Bist du sicher, dass du deine digitale Identität zurücksetzen möchtest?" + "Bist du sicher, dass du deine Identität zurücksetzen möchtest?" "Es ist ein unbekannter Fehler aufgetreten. Bitte überprüfe das Passwort deines Kontos und versuche es erneut." "Eingeben…" - "Bestätige, dass du deine digitale Identität zurücksetzen möchtest." + "Bestätige, dass du deine Identität zurücksetzen möchtest." "Gib dein Passwort ein, um fortzufahren" 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 0f4584271c..ca9adbc5ac 100644 --- a/features/securebackup/impl/src/main/res/values-el/translations.xml +++ b/features/securebackup/impl/src/main/res/values-el/translations.xml @@ -12,7 +12,6 @@ "Εισαγωγή κλειδιού ανάκτησης" "Ο αποθηκευτικός χώρος κλειδιών σου δεν είναι συγχρονισμένος αυτήν τη στιγμή." "Λήψη κλειδιού ανάκτησης" - "Οι συνομιλίες σας αποθηκεύονται αυτόματα με κρυπτογράφηση από άκρο σε άκρο. Για να επαναφέρετε αυτό το αντίγραφο ασφαλείας και να διατηρήσετε την ψηφιακή σας ταυτότητα όταν χάσετε την πρόσβαση σε όλες τις συσκευές σας, θα χρειαστείτε το κλειδί ανάκτησης. " "Άνοιγμα %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 860d017781..987fd88190 100644 --- a/features/securebackup/impl/src/main/res/values-et/translations.xml +++ b/features/securebackup/impl/src/main/res/values-et/translations.xml @@ -2,17 +2,16 @@ "Lülita võtmete varundamine välja" "Lülita võtmete varundamine sisse" - "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." + "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." "Krüptovõtmete varundus" - "Sinu vestluste varundamiseks peab võtmehoidla olema sisselülitatud." + "Taastamise seadistamiseks 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 digitaalsele identiteedile 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 krüptoidentiteedile ja sõnumite ajaloole." "Sisesta taastevõti" "Sinu krüptovõtmete varundus pole hetkel enam sünkroonis." - "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." + "Seadista andmete taastamine" "Ava %1$s töölauaga seadmes" "Logi uuesti sisse oma kasutajakontole" "Kui sul palutakse seadet verifitseerida, vali %1$s" @@ -24,12 +23,12 @@ "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 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:" + "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:" "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?" @@ -59,12 +58,12 @@ "Loo oma taastevõti" "Ära jaga seda kellegagi" "Andmete taastamise seadistamine õnnestus" - "Seadista taastevõti" + "Seadista andmete taastamine" "Jah, lähtesta nüüd" "See tegevus on tagasipöördumatu." - "Kas sa oled kindel, et soovid oma digitaalse identiteedi lähtestada?" + "Kas sa oled kindel, et soovid oma andmete krüptimist lähtestada?" "Tekkis teadmata viga. Palun kontrolli, kas sinu kasutajakonto salasõna on õige ja proovi uuesti." "Sisesta…" - "Palun kinnita, et soovid oma digitaalse identiteedi lähtestada." + "Palun kinnita, et soovid oma andmete krüptimist 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 bd7a43ffba..c16e5bd1d8 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-hr/translations.xml b/features/securebackup/impl/src/main/res/values-hr/translations.xml index 9c470b8895..3f146d279f 100644 --- a/features/securebackup/impl/src/main/res/values-hr/translations.xml +++ b/features/securebackup/impl/src/main/res/values-hr/translations.xml @@ -2,17 +2,16 @@ "Brisanje pohrane ključeva" "Uključivanje sigurnosnog kopiranja" - "To će vam omogućiti pregled povijesti razgovora na svim novim uređajima i potrebno je za sigurnosnu kopiju razgovora i digitalnog identiteta. %1$s ." + "Sigurno pohranite svoj kriptografski identitet i ključeve poruka na poslužitelju. To će vam omogućiti pregled povijesti poruka na svim novim uređajima. %1$s." "Pohrana ključeva" "Za postavljanje oporavka mora biti uključena pohrana ključeva." "Prenesi ključeve s ovog uređaja" "Dopusti pohranu ključeva" "Promjena ključa za oporavak" - "Vaši se razgovori automatski sigurnosno kopiraju enkripcijom od početka do kraja. Da biste vratili ovu sigurnosnu kopiju i zadržali svoj digitalni identitet kada izgubite pristup svim svojim uređajima, trebat će vam ključ za oporavak." + "Ako ste izgubili sve postojeće uređaje, oporavite svoj kriptografski identitet i povijest poruka pomoću ključa za oporavak." "Unesi ključ za oporavak" "Vaša pohrana ključeva trenutačno nije sinkronizirana." - "ključ za oporavak" - "Vaši se razgovori automatski sigurnosno kopiraju enkripcijom od početka do kraja. Da biste vratili ovu sigurnosnu kopiju i zadržali svoj digitalni identitet kada izgubite pristup svim svojim uređajima, trebat će vam ključ za oporavak." + "Postavljanje oporavka" "Otvorite %1$s na stolnom uređaju" "Ponovno se prijavite na svoj račun" "Kada se od vas zatraži da potvrdite svoj uređaj, odaberite %1$s" @@ -25,9 +24,9 @@ "Izgubit ćete svu povijest poruka koja je pohranjena samo na poslužitelju" "Morat ćete ponovno potvrditi sve svoje postojeće uređaje i kontakte" "Poništite svoj identitet samo ako nemate pristup drugom prijavljenom uređaju i ako ste izgubili ključ za oporavak." - "Ne možete potvrditi? Morat ćete resetirati svoj digitalni identitet." - "Izbriši" - "Izgubit ćete svoju šifriranu povijest razgovora i morat ćete resetirati svoj digitalni identitet ako uklonite sve svoje uređaje." + "Ne možete potvrditi? Morat ćete poništiti svoj identitet." + "Isključi" + "Izgubit ćete šifrirane poruke ako se odjavite sa svih uređaja." "Jeste li sigurni da želite isključiti sigurnosno kopiranje?" "Brisanjem pohrane ključeva uklonit ćete svoj kriptografski identitet i ključeve poruka s poslužitelja te isključiti sljedeće sigurnosne značajke:" "Na novim uređajima nećete imati šifriranu povijest poruka" @@ -59,12 +58,12 @@ "Generirajte svoj ključ za oporavak" "Ne dijelite ovo ni s kim!" "Postavljanje oporavka je uspjelo" - "ključ za oporavak" + "Postavljanje oporavka" "Da, poništi sada" "Ovaj je proces nepovratan." - "Jeste li sigurni da želite resetirati svoj digitalni identitet?" + "Jeste li sigurni da želite poništiti svoj identitet?" "Došlo je do nepoznate pogreške. Provjerite je li zaporka vašeg računa ispravna i pokušajte ponovno." "Unos…" - "Potvrdite da želite resetirati svoj digitalni identitet." + "Potvrdite da želite poništiti svoj identitet." "Unesite zaporku računa kako biste nastavili" 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 72c8c90809..7a7dd041fe 100644 --- a/features/securebackup/impl/src/main/res/values-ja/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ja/translations.xml @@ -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 99bbcc8159..fb30d786c0 100644 --- a/features/securebackup/impl/src/main/res/values-pl/translations.xml +++ b/features/securebackup/impl/src/main/res/values-pl/translations.xml @@ -2,35 +2,34 @@ "Wyłącz backup" "Włącz backup" - "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." + "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" "Magazyn kluczy" - "Magazyn kluczy musi być włączony, aby włączyć archiwizowanie czatów." + "Magazyn kluczy musi być włączony, aby włączyć przywracanie." "Prześlij klucze z tego urządzenia" "Zezwól na magazynowanie kluczy" "Zmień 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." + "Odzyskaj swoją tożsamość kryptograficzną i historię wiadomości za pomocą klucza przywracania, jeśli utraciłeś dostęp do wszystkich swoich urządzeń." "Wprowadź klucz przywracania" "Magazyn kluczy nie jest zsynchronizowany." - "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." + "Skonfiguruj przywracanie" "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" - "“Zresetuj wszystko”" + "“Resetuj wszystko”" "Postępuj zgodnie z instrukcjami, aby utworzyć nowy klucz przywracania" "Zapisz nowy klucz przywracania w menedżerze haseł lub notatce szyfrowanej" - "Zresetuj szyfrowanie swojego konta za pomocą drugiego urządzenia" + "Resetuj szyfrowanie swojego konta za pomocą drugiego urządzenia" "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 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" + "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" "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ł." @@ -59,12 +58,12 @@ "Wygeneruj klucz przywracania" "Nie udostępniaj tego nikomu!" "Skonfigurowano przywracanie pomyślnie" - "Uzyskaj klucz przywracania" + "Skonfiguruj przywracanie" "Tak, zresetuj teraz" "Tego procesu nie można odwrócić." - "Czy na pewno chcesz zresetować swoją tożsamość cyfrową?" + "Czy na pewno chcesz zresetować szyfrowanie?" "Wystąpił nieznany błąd. Sprawdź, czy hasło jest poprawne i spróbuj ponownie." "Wprowadź…" - "Potwierdź, że chcesz zresetować swoją tożsamość cyfrową." + "Potwierdź, że chcesz zresetować szyfrowanie." "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 66051ca04b..1e5d8b03ab 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" - "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." + "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." "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." - "Chave de recuperação" + "Configurar 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" - "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:" + "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:" "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" - "Chave de recuperação" + "Configurar 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 2351a54923..f8adf79229 100644 --- a/features/securebackup/impl/src/main/res/values-ro/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ro/translations.xml @@ -2,17 +2,16 @@ "Dezactivați backupul" "Activați backupul" - "Acest lucru vă va permite să vizualizați istoricul camerelor pe orice dispozitiv nou și este necesar pentru backupul mesajelor și al identității digitale. %1$s." + "Stocați identitatea criptografică și cheile de mesaje în siguranță pe server. Acest lucru vă va permite să vizualizați mesajele anterioare pe orice dispozitiv nou. %1$s." "Backup" - "Stocarea cheilor trebuie activată pentru a face un backup al mesajelor." + "Stocarea cheilor trebuie activată pentru a configura recuperarea." "Încărcați cheile de pe acest dispozitiv" "Permiteți stocarea cheilor" "Schimbați cheia de recuperare" - "Mesajele dumneavoastră sunt copiate 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." + "Recuperați-vă identitatea criptografică și mesajele anterioare cu o cheie de recuperare dacă ați pierdut toate dispozitivele existente." "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." + "Configurați recuperarea" "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" @@ -24,8 +23,8 @@ "Detaliile contului, contactele, preferințele și lista de chat vor fi păstrate" "Veți pierde mesajele anterioare care au fost stocate doar pe server" "Va trebui să verificați din nou toate dispozitivele și contactele existente" - "Resetați-vă identitatea digitală numai dacă nu aveți acces la un alt dispozitiv conectat și ați pierdut cheia de recuperare." - "Nu puteți confirma? Va trebui să vă resetați identitatea digitală." + "Resetați-vă identitatea numai dacă nu aveți acces la un alt dispozitiv conectat și ați pierdut cheia de recuperare." + "Nu puteți confirma? Va trebui să vă resetați identitatea." "Dezactivare" "Veți pierde mesajele criptate dacă sunteți deconectat de pe toate dispozitivele." "Sunteți sigur că doriți să dezactivați backup-ul?" @@ -59,12 +58,12 @@ "Generați cheia de recuperare" "Nu împărtășiți cheia cu nimeni!" "Configurarea recuperării a reușit" - "Obțineți cheia de recuperare" + "Configurați recuperarea" "Da, resetați acum" "Acest proces este ireversibil." - "Sunteți sigur că doriți să vă resetați identitatea digitală?" + "Sunteți sigur că doriți să vă resetați identitatea?" "S-a produs o eroare necunoscută. Vă rugăm să verificați dacă parola contului dvs. este corectă și să încercați din nou." "Introduceți…" - "Confirmați că doriți să vă resetați identitatea digitală." + "Confirmați că doriți să vă resetați identitatea." "Introduceți parola contului pentru a continua" 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..ec5237235e 100644 --- a/features/securebackup/impl/src/main/res/values-ru/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ru/translations.xml @@ -19,7 +19,7 @@ "«Сбросить все»" "Следуйте инструкциям, чтобы создать новый ключ восстановления" "Сохраните новый ключ восстановления в менеджере паролей или зашифрованной заметке" - "Сбросьте шифрование Вашего аккаунта, используя другое устройство" + "Сбросьте шифрование вашего аккаунта, используя другое устройство" "Продолжить сброс" "Данные вашего аккаунта, контакты, настройки и список чатов будут сохранены" "Вы потеряете историю тех сообщений, которые хранятся только на сервере" 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 3e92a39df0..b02d5ecfbb 100644 --- a/features/securebackup/impl/src/main/res/values-sk/translations.xml +++ b/features/securebackup/impl/src/main/res/values-sk/translations.xml @@ -26,7 +26,7 @@ "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úť" - "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." + "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:" "Na nových zariadeniach nebudete mať zašifrovanú históriu správ" 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 82a96fcab9..218e6d64fd 100644 --- a/features/securebackup/impl/src/main/res/values-uk/translations.xml +++ b/features/securebackup/impl/src/main/res/values-uk/translations.xml @@ -12,7 +12,6 @@ "Введіть ключ відновлення" "Сховище ключів наразі не синхронізовано." "Налаштувати відновлення" - "Ваші чати автоматично резервуються з використанням наскрізного шифрування. Щоб відновити цю резервну копію та зберегти свою цифрову ідентичність у разі втрати доступу до всіх своїх пристроїв, вам знадобиться ключ відновлення." "Відкрийте %1$s на комп\'ютері" "Увійдіть до вашого облікового запису знову" "Коли вас попросять підтвердити пристрій, виберіть %1$s" diff --git a/features/securebackup/impl/src/main/res/values-uz/translations.xml b/features/securebackup/impl/src/main/res/values-uz/translations.xml index 3a93e264dd..9e90d2f441 100644 --- a/features/securebackup/impl/src/main/res/values-uz/translations.xml +++ b/features/securebackup/impl/src/main/res/values-uz/translations.xml @@ -2,17 +2,16 @@ "Zaxiralashni o\'chirib qo\'ying" "Zaxiralashni yoqing" - "Bu sizga chat tarixingizni har qanday yangi qurilmalarda ko‘rish imkonini beradi hamda chatlar zaxirasi va raqamli identifikatsiya uchun talab qilinadi. %1$s." + "Kryptografik shaxsiyatingizni va xabar kalitlaringizni serverda xavfsiz saqlang. Bu sizga har qanday yangi qurilmalarda xabar tarixingizni ko\'rish imkonini beradi. %1$s." "Kalitlar ombori" - "Chatlarni zaxiralash uchun kalit xotirasi yoqilishi kerak." + "Tiklashni sozlash uchun kalitlar xotirasini yoqish kerak." "Bu qurilmadan kalitlarni yuklash" "Kalit saqlashga ruxsat berish" "Qayta tiklash kalitini o\'zgartiring" - "Chatlaringiz avtomatik ravishda boshidan oxirigacha shifrlash bilan zaxiralanadi. Bu zaxirani tiklash va barcha qurilmalaringizdan foydalana olmay qolganingizda raqamli identifikatoringizni saqlab qolish uchun sizga tiklash kaliti kerak bo‘ladi." + "Agar barcha mavjud qurilmalaringizni yoʻqotgan boʻlsangiz, tiklash kaliti yordamida kriptografik shaxsingizni va xabarlar tarixingizni qayta tiklang." "Tiklash kalitini kiriting" "Kalit xotirasi hozirda sinxronlanmagan." "Qayta tiklashni sozlang" - "Chatlaringiz avtomatik ravishda boshidan oxirigacha shifrlash bilan zaxiralanadi. Bu zaxirani tiklash va barcha qurilmalaringizdan foydalana olmay qolganingizda raqamli identifikatoringizni saqlab qolish uchun sizga tiklash kaliti kerak bo‘ladi." "%1$s ni kompyuterda oching" "Hisobingizga qaytadan kiring" "Qurilmangizni tasdiqlash soʻralganda, %1$s ni tanlang" @@ -24,12 +23,12 @@ "Hisob maʼlumotlaringiz, kontaktlaringiz, sozlamalaringiz va suhbatlar roʻyxatingiz saqlanib qoladi" "Faqat serverda saqlangan har qanday xabarlar tarixi oʻchib ketadi" "Barcha mavjud qurilma va kontaktlarni qayta tasdiqlashingiz kerak boʻladi" - "Agar boshqa tasdiqlangan qurilmaga kira olmasangiz va zaxira kalitingiz bo‘lmasa, raqamli identifikatoringizni asliga qaytaring." - "Tasdiqlay olmayapsizmi? Raqamli identifikatoringizni asliga qaytarishingiz kerak." + "Agar boshqa hisobga kirilgan qurilmaga kira olmasangiz va tiklash kaliti yo‘qolgan bo‘lsa, shaxsingizni tiklang." + "Tasdiqlanmadimi? Shaxsingizni tiklashingiz kerak." "O\'chirish" - "Agar barcha qurilmalaringizni olib tashlasangiz, shifrlangan chat tarixingizni yo‘qotasiz va raqamli identifikatoringizni asliga qaytarishingiz kerak bo‘ladi." - "Kalitlar omborini o‘chirib tashlashni xohlaysizmi?" - "Kalit xotirasini o‘chirish raqamli identifikatsiya va xabar kalitlaringizni serverdan olib tashlaydi hamda quyidagi xavfsizlik funksiyalarini faolsizlantiradi:" + "Agar barcha qurilmalardan chiqqan boʻlsangiz, shifrlangan xabarlaringizni yoʻqotasiz." + "Haqiqatan ham zaxiralashni o‘chirib qo‘ymoqchimisiz?" + "Zaxiralashni o‘chirib qo‘ysangiz, joriy shifrlash kaliti zaxira nusxasi o‘chiriladi va boshqa xavfsizlik funksiyalari o‘chiriladi. Bunday holda siz:" "Yangi qurilmalarda shifrlangan xabarlar tarixi mavjud emas" "Agar tizimdan chiqqan boʻlsangiz, shifrlangan xabarlaringizga kirish huquqini yoʻqotasiz%1$s hamma joyda" "Haqiqatan ham zaxiralashni o‘chirib qo‘ymoqchimisiz?" @@ -62,9 +61,9 @@ "Qayta tiklashni sozlang" "Ha, hozir asliga qaytarish" "Bu jarayonni ortga qaytarib boʻlmaydi." - "Haqiqatan ham raqamli identifikatoringizni tiklamoqchimisiz?" + "Haqiqatan ham shaxsingizni qayta tiklamoqchimisiz?" "Noma’lum xato yuz berdi. Iltimos, hisobingiz parolining to‘g‘riligini tekshiring va qaytadan urinib ko‘ring." "Kirish…" - "Raqamli identifikatoringizni asliga qaytarmoqchi ekaningizni tasdiqlang." + "Shaxsingizni tiklashni tasdiqlang." "Davom etish uchun hisobingiz parolini kiriting" diff --git a/features/securebackup/impl/src/main/res/values-vi/translations.xml b/features/securebackup/impl/src/main/res/values-vi/translations.xml index 74784f8921..70913a5e24 100644 --- a/features/securebackup/impl/src/main/res/values-vi/translations.xml +++ b/features/securebackup/impl/src/main/res/values-vi/translations.xml @@ -4,28 +4,10 @@ "Bật tính năng sao lưu" "Điều này cho phép bạn xem lịch sử trò chuyện trên bất kỳ thiết bị mới nào và cần thiết để sao lưu trò chuyện cũng như danh tính kỹ thuật số. %1$s." "Lưu trữ khóa" - "Bạn cần bật tính năng lưu trữ khóa để sao lưu các cuộc trò chuyện của mình." - "Tải lên các khóa từ thiết bị này" - "Cho phép lưu trữ khóa" "Thay đổi khóa khôi phục." - "Các cuộc trò chuyện của bạn được tự động sao lưu bằng mã hóa đầu cuối. Để khôi phục bản sao lưu này và giữ lại danh tính kỹ thuật số của bạn khi bạn mất quyền truy cập vào tất cả các thiết bị, bạn sẽ cần khóa khôi phục." "Nhập mã khôi phục." "Kho lưu trữ khóa của bạn hiện đang không đồng bộ." "Lấy khóa khôi phục." - "Các cuộc trò chuyện của bạn được tự động sao lưu bằng mã hóa đầu cuối. Để khôi phục bản sao lưu này và giữ lại danh tính kỹ thuật số của bạn khi bạn mất quyền truy cập vào tất cả các thiết bị, bạn sẽ cần khóa khôi phục." - "Mở %1$s trên máy tính để bàn" - "Đăng nhập lại vào tài khoản của bạn" - "Khi được yêu cầu xác minh thiết bị của bạn, chọn %1$s" - "“Khôi phục tất cả”" - "Hãy làm theo hướng dẫn để tạo khóa khôi phục mới." - "Hãy lưu khóa khôi phục mới của bạn vào trình quản lý mật khẩu hoặc ghi chú được mã hóa." - "Đặt lại mã hóa cho tài khoản của bạn bằng một thiết bị khác." - "Tiếp tục đặt lại" - "Thông tin tài khoản, danh bạ, tùy chọn và danh sách trò chuyện của bạn sẽ được lưu giữ." - "Bạn sẽ mất toàn bộ lịch sử tin nhắn chỉ được lưu trữ trên máy chủ." - "Bạn sẽ cần xác minh lại tất cả các thiết bị và danh bạ hiện có của mình." - "Chỉ nên đặt lại danh tính kỹ thuật số của bạn nếu bạn không có quyền truy cập vào thiết bị đã được xác minh khác và bạn không có khóa khôi phục." - "Không thể xác nhận? Bạn cần phải thiết lập lại danh tính kỹ thuật số của mình." "Xoá" "Bạn sẽ mất các tin nhắn đã mã hóa nếu bạn đăng xuất khỏi tất cả các thiết bị." "Bạn có chắc muốn xóa lưu trữ khóa không?" diff --git a/features/securebackup/impl/src/main/res/values-zh-rTW/translations.xml b/features/securebackup/impl/src/main/res/values-zh-rTW/translations.xml index 9b791eb938..85061fe044 100644 --- a/features/securebackup/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/securebackup/impl/src/main/res/values-zh-rTW/translations.xml @@ -2,17 +2,16 @@ "關閉備份功能" "開啟備份功能" - "此舉將讓您能在任何新裝置上檢視聊天記錄,且對於備份聊天內容及數位身分而言是必要的。%1$s。" + "在伺服器上安全地儲存您的密碼學身份與訊息金鑰。這將讓您可以在任何新裝置上檢視訊息歷史紀錄。%1$s" "金鑰儲存空間" - "必須開啟金鑰儲存空間才能備份您的聊天。" + "必須開啟金鑰儲存空間才能設定復原。" "從此裝置上傳金鑰" "允許金鑰儲存空間" "變更復原金鑰" - "您的聊天會自動以端到端加密方式進行備份。若您無法存取所有裝置,欲還原此備份並保留您的數位身分,您將需要使用還原金鑰。" + "若您遺失了您現有的所有裝置,請使用復原金鑰來還原您的密碼學身份與訊息歷史紀錄。" "輸入復原金鑰" "您的金鑰儲存空間目前並未同步。" - "取得還原金鑰" - "您的聊天會自動使用端到端加密備份。若您失去對您所有裝置的存取權,且要還原此備份並保留您的數位身份的話,您就會需要您的還原金鑰。" + "設定復原" "在桌上型裝置中開啟 %1$s" "再次登入您的帳號" "當要求驗證您的裝置時,請選取 %1$s" @@ -24,12 +23,12 @@ "您的帳號詳細資訊、聯絡人、偏好設定與聊天清單都會保留" "您將會遺失僅儲存在伺服器上的任何訊息歷史紀錄" "您將需要再次驗證所有現有裝置與聯絡人" - "僅當您無法存取其他已驗證的裝置且沒有還原金鑰時才重設您的數位身份。" - "無法確認?您需要重設數位身份。" - "刪除" - "若您移除所有裝置,您將遺失加密的聊天記錄,並需重設您的數位身分。" - "您確定您要刪除金鑰儲存空間嗎?" - "刪除金鑰儲存空間會從伺服器移除您的數位身份與訊息金鑰,並關閉以下安全性功能:" + "僅當您無法存取其他已登入裝置且遺失復原金鑰時才重設您的身份。" + "無法確認?您需要重設身份。" + "關閉" + "若您登出所有裝置,您將失去加密訊息。" + "您確定您要關閉備份嗎?" + "刪除金鑰儲存空間會從伺服器移除您的密碼學身份與訊息金鑰,並關閉以下安全性功能:" "您將無法在新裝置上存取加密訊息歷史紀錄" "若您徹底登出 %1$s,您將無法存取加密訊息" "您確定要關閉金鑰儲存空間並刪除它嗎?" @@ -59,12 +58,12 @@ "產生您的復原金鑰" "不要與任何人分享!" "復原設定成功" - "取得還原金鑰" + "設定復原" "是的,立刻重設" "此過程不可逆。" "您確定您想要重設您的身份嗎?" "發生了未知錯誤。請檢查您帳號的密碼是否正確,然後再試一次。" "輸入……" - "確認您要重設您的數位身份。" + "確認您要重設您的身份。" "輸入您帳號的密碼以繼續" 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 459291614e..8d86e496cb 100644 --- a/features/securebackup/impl/src/main/res/values-zh/translations.xml +++ b/features/securebackup/impl/src/main/res/values-zh/translations.xml @@ -2,69 +2,68 @@ "关闭备份" "开启备份" - "这将允许你在新设备上查看聊天历史, 这是备份聊天与数字身份所必需的。%1$s。" + "将您的密码学身份和消息密钥安全地存储在服务器上。这样您就可以在任何新设备上查看您的消息历史记录。%1$s。" "密钥存储" - "必须启用密钥存储才能备份聊天。" + "必须打开密钥存储才能设置恢复。" "从此设备上传密钥" "允许密钥存储" "更改恢复密钥" - "你的聊天已被端到端加密自动备份。如果你无法访问所有设备,则需要使用恢复密钥并保留数字身份。" + "如果您丢失了所有现有设备,使用恢复密钥恢复您的密码学身份和消息历史记录。" "输入恢复密钥" - "当前密钥存储已脱离同步。" + "您的密钥存储当前不同步。" "获取恢复密钥" - "你的聊天已被端到端加密自动备份。如果你无法访问所有设备,则需要使用恢复密钥恢复备份并保留数字身份。" "在桌面设备中打开 %1$s" - "再次登录你的账户" - "当要求验证你的设备时,选择 %1$s" - "“重置全部”" + "再次登录您的账户" + "当要求验证您的设备时,选择 %1$s" + "「全部重置」" "按照说明创建新的恢复密钥" "将新的恢复密钥保存在密码管理器或加密备忘录中" - "使用其它设备重置账户的加密" + "使用其他设备重置账户的加密" "继续重置" - "你的账户信息、联系人、偏好和聊天列表将被保留" - "你将丢失所有仅存储在服务器上的消息历史" - "你将需要再次验证所有现有设备与联系人" - "仅当你无法访问其它已登录的设备并且丢失了恢复密钥时才重置数字身份。" - "无法确认?你需要重置数字身份。" - "删除" - "如果移除所有设备,你将丢失加密聊天历史,并且需要重置数字身份。" - "你确定要关闭密钥存储?" - "删除密钥存储将移除你的数字身份并关闭以下安全功能:" + "您的账户信息、联系人、偏好设置和聊天列表将被保留" + "您将丢失现有的消息历史记录" + "您将需要再次验证所有您的现有设备和联系人" + "仅当您无法访问其他已登录设备并且丢失了恢复密钥时才重置您的数字身份。" + "无法确认?那么你需要重置您的数字身份。" + "关闭" + "如果您登出所有设备,您的加密消息将丢失。" + "您确定要关闭备份吗?" + "关闭备份将删除您当前的加密密钥备份并关闭其他安全功能。在这种情况下,你将:" "新设备上没有加密消息的历史记录" - "如果你在所有位置注销 %1$s,将无法访问加密消息" - "你确定要关闭并删除密钥存储?" - "如果你丢失了现有恢复密钥,请重新获取。旧密钥将随恢复密钥更改后失效。" + "如果您在所有设备上登出了 %1$s,那将无法访问加密消息" + "您确定要关闭备份吗?" + "如果您丢失了现有的恢复密钥,请获取新的恢复密钥。更改恢复密钥后,您的旧密钥将不再起作用。" "生成新的恢复密钥" "请勿与任何人分享!" "恢复密钥已更改" "更改恢复密钥?" "创建新的恢复密钥" - "确保没人能看到此界面!" - "请重试以确认访问密钥存储。" + "确保没有人能看到这个界面!" + "请重试以确认访问您的密钥存储。" "恢复密钥不正确" - "如果你有安全密钥或安全口令也同样可用。" - "输入…" + "如果您有安全密钥或安全短语,也可以用。" + "输入……" "丢失了恢复密钥?" "恢复密钥已确认" "输入恢复密钥" "恢复密钥已复制" - "正在生成…" + "正在生成……" "保存恢复密钥" "将此恢复密钥保存在安全的地方,例如密码管理器、加密笔记或物理保险箱。" "点击复制恢复密钥" - "保存恢复密钥到安全的地方。" - "此步骤之后将无法访问新的恢复密钥。" - "你是否已保存恢复密钥?" - "密钥存储受恢复密钥保护。如果在设置后需要新的恢复密钥,则可以通过选择“更改恢复密钥”重新创建。" + "保存您的恢复密钥" + "完成此步骤后,您将无法访问新的恢复密钥。" + "您保存了恢复密钥吗?" + "您的聊天备份受恢复密钥保护。如果您在安装后需要新的恢复密钥,则可以通过选择「更改恢复密钥」来重新创建。" "生成恢复密钥" "请勿与任何人分享!" "恢复设置成功" "获取恢复密钥" - "是,立即重置" + "是的,立即重置" "此过程不可逆。" - "你确定要重置数字身份?" - "发生未知错误。请检查你的账户密码是否正确并重试。" - "输入…" - "请确认你要重置数字身份。" - "输入账户的密码以继续" + "您确定要重置您的数字身份吗?" + "发生未知错误。请检查您的帐户密码是否正确,然后重试。" + "输入……" + "确认您要重置您的数字身份。" + "输入您的账户密码以继续" 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 f9729f74f0..d9324fdb91 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,19 +6,16 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey import io.element.android.libraries.architecture.AsyncAction @@ -29,54 +26,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 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() + @Test - fun `back key pressed - calls onBackClick`() = runAndroidComposeUiTest { + fun `back key pressed - calls onBackClick`() { ensureCalledOnce { callback -> - setSecureBackupEnterRecoveryKeyView( + rule.setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(), onBackClick = callback, ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `back button clicked - calls onBackClick`() = runAndroidComposeUiTest { + fun `back button clicked - calls onBackClick`() { ensureCalledOnce { callback -> - setSecureBackupEnterRecoveryKeyView( + rule.setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(), onBackClick = callback, ) - pressBack() + rule.pressBack() } } @Test @Config(qualifiers = "h1024dp") - fun `tapping on Continue when key is valid - calls expected action`() = runAndroidComposeUiTest { + fun `tapping on Continue when key is valid - calls expected action`() { val recorder = EventsRecorder() - setSecureBackupEnterRecoveryKeyView( + rule.setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit) } @Test - fun `entering a char emits the expected event`() = runAndroidComposeUiTest { + fun `entering a char emits the expected event`() { val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() - setSecureBackupEnterRecoveryKeyView( + rule.setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) - onNodeWithText(keyValue).performTextInput("X") + rule.onNodeWithText(keyValue).performTextInput("X") recorder.assertSingle( SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange("X$keyValue") ) @@ -84,43 +85,43 @@ class SecureBackupEnterRecoveryKeyViewTest { @Test @Config(qualifiers = "h1024dp") - fun `toggling the visibility of the textfield changes it`() = runAndroidComposeUiTest { + fun `toggling the visibility of the textfield changes it`() { val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() - setSecureBackupEnterRecoveryKeyView(aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder)) + rule.setSecureBackupEnterRecoveryKeyView(aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder)) // Initially, the text field should be visible - onNodeWithText(keyValue).assertExists() + rule.onNodeWithText(keyValue).assertExists() - onNodeWithContentDescription(activity!!.getString(CommonStrings.a11y_hide_password)).performClick() + rule.onNodeWithContentDescription(rule.activity.getString(CommonStrings.a11y_hide_password)).performClick() - waitForIdle() + rule.waitForIdle() recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility(false)) } @Test - fun `validating from keyboard emits the expected event`() = runAndroidComposeUiTest { + fun `validating from keyboard emits the expected event`() { val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() - setSecureBackupEnterRecoveryKeyView( + rule.setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) - onNodeWithText(keyValue).performImeAction() + rule.onNodeWithText(keyValue).performImeAction() recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit) } @Test - fun `when submit action succeeds - calls onDone`() = runAndroidComposeUiTest { + fun `when submit action succeeds - calls onDone`() { ensureCalledOnce { callback -> - setSecureBackupEnterRecoveryKeyView( + rule.setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Success(Unit)), onDone = callback, ) } } - private fun AndroidComposeUiTest.setSecureBackupEnterRecoveryKeyView( + private fun AndroidComposeTestRule.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 ce5972f66b..6cfd061103 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.performTextInput -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.ui.strings.CommonStrings @@ -25,59 +22,64 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ResetIdentityPasswordViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `pressing the back HW button invokes the expected callback`() = runAndroidComposeUiTest { + fun `pressing the back HW button invokes the expected callback`() { ensureCalledOnce { - setResetPasswordView( + rule.setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), onBack = it, ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `clicking on the back navigation button invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on the back navigation button invokes the expected callback`() { ensureCalledOnce { - setResetPasswordView( + rule.setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), onBack = it, ) - pressBack() + rule.pressBack() } } @Test - fun `clicking 'Reset identity' confirms the reset`() = runAndroidComposeUiTest { + fun `clicking 'Reset identity' confirms the reset`() { val eventsRecorder = EventsRecorder() - setResetPasswordView( + rule.setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = eventsRecorder), ) - onNodeWithText("Password").performTextInput("A password") + rule.onNodeWithText("Password").performTextInput("A password") - clickOn(CommonStrings.action_reset_identity) + rule.clickOn(CommonStrings.action_reset_identity) eventsRecorder.assertSingle(ResetIdentityPasswordEvent.Reset("A password")) } @Test - fun `modifying the password dismisses the error state`() = runAndroidComposeUiTest { + fun `modifying the password dismisses the error state`() { val eventsRecorder = EventsRecorder() - setResetPasswordView( + rule.setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("A failure")), eventSink = eventsRecorder), ) - onNodeWithText("Password").performTextInput("A password") + rule.onNodeWithText("Password").performTextInput("A password") eventsRecorder.assertSingle(ResetIdentityPasswordEvent.DismissError) } } -private fun AndroidComposeUiTest.setResetPasswordView( +private fun AndroidComposeTestRule.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 0126d0d879..a913a9af27 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.securebackup.impl.R import io.element.android.libraries.ui.strings.CommonStrings @@ -23,71 +20,76 @@ 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 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() + @Test - fun `pressing the back HW button invokes the expected callback`() = runAndroidComposeUiTest { + fun `pressing the back HW button invokes the expected callback`() { ensureCalledOnce { - setResetRootView( + rule.setResetRootView( ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), onBack = it, ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `clicking on the back navigation button invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on the back navigation button invokes the expected callback`() { ensureCalledOnce { - setResetRootView( + rule.setResetRootView( ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), onBack = it, ) - pressBack() + rule.pressBack() } } @Test @Config(qualifiers = "h720dp") - fun `clicking Continue displays the confirmation dialog`() = runAndroidComposeUiTest { + fun `clicking Continue displays the confirmation dialog`() { val eventsRecorder = EventsRecorder() - setResetRootView( + rule.setResetRootView( ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder), ) - clickOn(R.string.screen_encryption_reset_action_continue_reset) + rule.clickOn(R.string.screen_encryption_reset_action_continue_reset) eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue) } @Test - fun `clicking 'Yes, reset now' confirms the reset`() = runAndroidComposeUiTest { + fun `clicking 'Yes, reset now' confirms the reset`() { ensureCalledOnce { - setResetRootView( + rule.setResetRootView( ResetIdentityRootState(displayConfirmationDialog = true, eventSink = {}), onContinue = it, ) - clickOn(R.string.screen_reset_encryption_confirmation_alert_action) + rule.clickOn(R.string.screen_reset_encryption_confirmation_alert_action) } } @Test - fun `clicking Cancel dismisses the dialog`() = runAndroidComposeUiTest { + fun `clicking Cancel dismisses the dialog`() { val eventsRecorder = EventsRecorder() - setResetRootView( + rule.setResetRootView( ResetIdentityRootState(displayConfirmationDialog = true, eventSink = eventsRecorder), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ResetIdentityRootEvent.DismissDialog) } } -private fun AndroidComposeUiTest.setResetRootView( +private fun AndroidComposeTestRule.setResetRootView( state: ResetIdentityRootState, onBack: () -> Unit = EnsureNeverCalled(), onContinue: () -> Unit = EnsureNeverCalled(), diff --git a/features/securityandprivacy/impl/src/main/res/values-ca/translations.xml b/features/securityandprivacy/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 4d541af9d1..0000000000 --- a/features/securityandprivacy/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - "É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 566c26b4af..659799693b 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 klyngen" + "Medlemmer af rummet" "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-de/translations.xml b/features/securityandprivacy/impl/src/main/res/values-de/translations.xml index c60efc009c..0bc6aa8877 100644 --- a/features/securityandprivacy/impl/src/main/res/values-de/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-de/translations.xml @@ -29,7 +29,6 @@ Wir empfehlen keine Verschlüsselung für Chats zu aktivieren, die jeder finden "Zugang" "Jeder in autorisierten Spaces kann beitreten." "Jeder in %1$s kann beitreten." - "Space Mitglieder" "Spaces werden zur Zeit nicht unterstützt." "Du benötigst eine Chat-Adresse, um den Chat im öffentlichen Verzeichnis sichtbar zu machen." "Adresse" diff --git a/features/securityandprivacy/impl/src/main/res/values-hr/translations.xml b/features/securityandprivacy/impl/src/main/res/values-hr/translations.xml index 4038a7a412..3dd3e7468d 100644 --- a/features/securityandprivacy/impl/src/main/res/values-hr/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-hr/translations.xml @@ -10,7 +10,6 @@ "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." - "Zatraži pridruživanje" "Svatko u %1$s može se pridružiti, ali svi ostali moraju zatražiti pristup." "Da, omogući šifriranje" "Nakon što se šifriranje za sobu omogući, više se neće moći onemogućiti. Povijest poruka bit će vidljiva samo članovima sobe otkad su pozvani ili otkad su joj se pridružili. @@ -21,15 +20,13 @@ Ne preporučujemo omogućavanje šifriranja za sobe koje svatko može pronaći i "Šifriranje" "Omogući sveobuhvatno šifriranje" "Svatko se može pridružiti." - "Bilo tko" - "Odaberite iz kojih se prostora članovi mogu pridružiti ovoj sobi bez pozivnice. %1$s" + "Odaberite iz kojih se prostora članovi mogu pridružiti ovoj sobi bez pozivnice. %1$s" "Upravljaj prostorima" "Samo pozvane osobe mogu se pridružiti." "Samo s pozivnicom" "Pristup" "Svatko tko se nalazi u ovlaštenim prostorima može se pridružiti." "Svatko u %1$s može se pridružiti." - "Članovi prostora" "Prostori trenutačno nisu podržani" "Trebat će vam adresa kako bi bila vidljiva u javnom direktoriju." "Adresa" 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 60415c2288..c62c4f2af4 100644 --- a/features/securityandprivacy/impl/src/main/res/values-pl/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-pl/translations.xml @@ -1,17 +1,10 @@ - "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." + "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." "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. @@ -20,31 +13,23 @@ 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 dołączyć." + "Każdy może znaleźć i dołączyć" "Każdy" - "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" + "Tylko osoby z zaproszeniem mogą dołączyć" + "Tylko zaproszenie" + "Dostęp do pokoju" "Przestrzenie nie są obecnie wspierane" - "Aby pokój był widoczny w katalogu pokoi publicznych, potrzebny jest adres pokoju." - "Adres" + "Aby pokój był widoczny w katalogu, potrzebny jest adres pokoju." + "Adres pokoju" "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" - "Każdy (historia jest publiczna)" - "Zmiany nie zmienią przeszłych wiadomości, tylko nowe. %1$s" + "Ktokolwiek" "Kto może czytać historię" - "Członkowie od kiedy zostali zaproszeni" - "Członkowie (cała historia)" + "Od momentu kiedy członkowie zostali zaproszeni" + "Członkowie od momentu włączenia tej opcji" "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" - "Adresy pokoi pomagają w znalezieniu i dołączeniu do pokoi i przestrzeni. Umożliwiają również łatwe udostępnianie ich innym." - "Widoczność" + "Widoczność pokoju" "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 2f3eaab319..83292637ce 100644 --- a/features/securityandprivacy/impl/src/main/res/values-ro/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-ro/translations.xml @@ -10,7 +10,6 @@ "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. @@ -21,26 +20,22 @@ 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." - "Oricine" - "Alegeți membrii căror spații se pot alătura acestei cameră fără invitație. %1$s" + "Alegeți membrii căror spații se pot alătura acestei camere 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" - "Membri de la momentul invitației" - "Membri (istoric complet)" + "Doar pentru membri, de la momentul în care au fost invitați" + "Doar pentru membri, după selectarea acestei opțiuni" "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-uk/translations.xml b/features/securityandprivacy/impl/src/main/res/values-uk/translations.xml index 0921887ccb..f8b96ee6c5 100644 --- a/features/securityandprivacy/impl/src/main/res/values-uk/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-uk/translations.xml @@ -8,10 +8,8 @@ "Інші простори, учасником яких ви не є" "Ваші простори" "Додати адресу" - "Будь-хто в авторизованих просторах може приєднатися, але всі інші повинні подати запит на доступ." "Усі повинні запитувати доступ." "Запит на приєднання" - "Будь-хто з %1$s може приєднатися, але всі інші повинні подати запит на доступ." "Так, увімкнути шифрування" "Після ввімкнення шифрування кімнати, його неможливо вимкнути, історію повідомлень бачитимуть лише учасники кімнати, яких було запрошено або які приєдналися до кімнати. Ніхто, крім учасників кімнати, не зможе прочитати повідомлення. Це може перешкоджати коректній роботі ботів і мостів. @@ -27,14 +25,11 @@ "Приєднатися можуть лише запрошені люди." "Лише запрошені" "Доступ" - "Долучитися може будь-хто, хто має доступ до авторизованих просторів." "Долучитися може будь-хто з %1$s." - "Учасники простору" "Простори наразі не підтримуються" "Вам знадобиться адреса кімнати, щоб зробити її видимою в каталозі." "Адреса" "Дозвольте, щоб цю кімнату можна було знайти за допомогою пошуку в каталозі загальнодоступних кімнат %1$s " - "Дозвольте знаходити вас за допомогою пошуку в публічному каталозі." "Видима в загальному каталозі" "Будь-хто (загальнодоступна історія)" "Зміни не вплинуть на попередні повідомлення, лише на нові. %1$s" @@ -44,7 +39,6 @@ "Адреси кімнат — це спосіб знайти кімнату та отримати до неї доступ. Це також гарантує, що ви можете легко поділитися своєю кімнатою з іншими. Ви можете опублікувати свою кімнату в каталозі загальнодоступних кімнат вашого домашнього сервера." "Публікація в кімнаті" - "Адреси — це спосіб знаходити кімнати та простори та отримувати до них доступ. Це також гарантує, що ви зможете легко ділитися ними з іншими." "Видимість" "Безпека й приватність" diff --git a/features/securityandprivacy/impl/src/main/res/values-uz/translations.xml b/features/securityandprivacy/impl/src/main/res/values-uz/translations.xml index 134378f4f5..2197e52905 100644 --- a/features/securityandprivacy/impl/src/main/res/values-uz/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-uz/translations.xml @@ -10,7 +10,6 @@ "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" - "Qo‘shilish uchun so‘rash" "%1$s ichidagi istalgan kishi qo‘shilishi mumkin, lekin qolganlar ruxsat so‘rashi kerak." "Ha, shifrlashni yoqish" "Yoqilgandan so‘ng, xona uchun shifrlashni o‘chirib bo‘lmaydi. Xabarlar tarixi faqat xona a’zolari taklif qilinganidan yoki xonaga qo‘shilganidan keyingi davrdan boshlab ko‘rinadi. Xona a’zolaridan tashqari hech kim xabarlarni o‘qiy olmaydi. Bu botlar va ko‘priklarning to‘g‘ri ishlashiga to‘sqinlik qilishi mumkin. @@ -20,7 +19,6 @@ Shu sababli, har kim topishi va qo‘shilishi mumkin bo‘lgan xonalar uchun shi "Shifrlash" "End-to-end shifrlashni yoqish" "Istalgan kishi topishi va qo‘shilishi mumkin" - "Har kim" "Qaysi maydonlar a’zolari bu xonaga taklifnomalarsiz kirishi mumkinligini tanlang. %1$s" "Maydonlarni boshqarish" "Odamlar faqat taklif qilingan taqdirdagina qo‘shilishi mumkin" @@ -28,18 +26,15 @@ Shu sababli, har kim topishi va qo‘shilishi mumkin bo‘lgan xonalar uchun shi "Xonaga kirish huquqi" "Ruxsat berilgan maydonlardagi istalgan kishi qo‘shilishi mumkin." "%1$s ichidagi istalgan kishi qo‘shilishi mumkin." - "Maydon a’zolari" "Hozirda maydonlar qo‘llab-quvvatlanmaydi" "Katalogda ko‘rinadigan qilish uchun xona manzili kerak bo‘ladi." "Manzil" "Bu xonani %1$s umumiy xonalar ro‘yxatidan qidirib topish imkoniyatini berish" "Umumiy katalogni qidirish orqali topishga ruxsat bering." "Umumiy xona ro‘yxatida ko‘rinadi" - "Har kim (tarix hammaga ochiq)" - "O‘zgarishlar avvalgi xabarlarga ta’sir qilmaydi, faqat yangilariga ta’sir qiladi.%1$s" "Tarixni kim o‘qiy oladi" - "Taklif qilinganidan beri a’zo" - "A’zolar (to‘liq tarix)" + "Taklif qilinganidan buyon faqat a’zolar" + "A’zolar faqat bu parametr tanlanganidan keyin" "Xona manzillari xonalarni topish va ularga kirish usullaridir. Bu shuningdek xonangizni boshqalar bilan oson ulashish imkonini beradi. Xonangizni o‘z homeserveringizning ommaviy xonalar ro‘yxatida e’lon qilishni tanlashingiz mumkin." "xona nashriyoti" diff --git a/features/securityandprivacy/impl/src/main/res/values-zh-rTW/translations.xml b/features/securityandprivacy/impl/src/main/res/values-zh-rTW/translations.xml index 5239585201..b9a93922f4 100644 --- a/features/securityandprivacy/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-zh-rTW/translations.xml @@ -2,16 +2,9 @@ "您需要地址才能在公開目錄中顯示。" "編輯地址" - "在此空間中,成員可無須邀請直接加入聊天室。" - "管理空間" - "(未知空間)" - "您尚非成員的其他空間" - "您的空間" "新增地址" - "任何在授權空間的人都可以加入,但其他人都必須提出申請。" "所有人都必須申請存取權。" "要求加入" - "任何在 %1$s 中的人都可以加入,但其他人都必須提出申請。" "是的,啟用加密" "啟用後就無法停用聊天室的加密,只有受邀的聊天室成員或加入聊天室後才能看到訊息歷史紀錄。 除了聊天室成員以外,任何人都不能讀取訊息。這可能會讓機器人與橋接無法正常運作。 @@ -22,29 +15,22 @@ "啟用端到端加密" "任何人都可以加入。" "任何人" - "選擇哪些空間的成員不需要邀請就可以加入此聊天室。%1$s" - "管理空間" "僅受邀者才能加入。" "僅限邀請" "存取權" - "任何位於已授權空間的人都可以加入。" - "任何在 %1$s 中的人都可以加入。" - "空間成員" "目前不支援空間" "您需要地址才能在公開目錄中顯示。" "地址" "允許透過搜尋 %1$s 公開聊天室目錄找到此聊天室" "允許其他人透過公開目錄找到。" "在公開目錄中可見" - "任何人(歷史紀錄公開)" - "變更不會影響先前的訊息,只會影響新訊息。%1$s" + "任何人" "誰可以讀取歷史紀錄" - "成員,邀請後" - "成員(完整歷史)" + "僅在成員被邀請後" + "選取此選項後僅限成員" "聊天室地址是尋找與存取聊天室的方法。也確保您可以輕鬆與其他人分享聊天室。 您可以選擇在家伺服器公開聊天室目錄中發佈您的聊天室。" "聊天室發佈" - "地址是尋找與存取聊天室與空間的一種方式。這也讓您可以輕鬆地與其他人分享這些資訊。" "能見度" "安全與隱私" 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 09a22f06b3..8b10638e25 100644 --- a/features/securityandprivacy/impl/src/main/res/values-zh/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-zh/translations.xml @@ -1,20 +1,20 @@ - "你需要一个地址才能使其在公共目录中可见。" + "您需要一个地址才能在公共目录中显示。" "编辑地址" "无需邀请即可加入的公共空间。" "管理空间" "(未知空间)" - "你尚不是其成员的其它空间" - "你的空间" + "您尚未加入的其他空间" + "您的空间" "添加地址" - "已授权空间内的任何成员都可以加入,其他人必须申请访问。" + "授权空间内任何成员均可加入,其他人员需申请访问权限。" "所有用户均需申请访问权限。" - "申请加入" - "%1$s 成员可以加入,但其他人员必须申请访问。" - "是,启用加密" - "一旦启用,就不能再禁用房间的加密功能。消息历史只能在房间成员被邀请或加入房间后才可见。 -除房间成员外,任何人都无法阅读消息。这可能会阻止机器人和桥接器正常工作。 + "请求加入" + "%1$s 成员可自由加入,其他人员需申请访问权限。" + "是的,启用加密" + "一旦启用,就不能再禁用房间的加密功能。消息历史记录只能在房间成员被邀请或加入房间后才可见。 +除房间成员外,任何人都无法阅读信息。这可能会妨碍机器人和网桥正常工作。 我们不建议对任何人都能找到并加入的房间启用加密。" "启用加密?" "加密一旦启用,就无法禁用。" @@ -22,16 +22,16 @@ "启用端到端加密" "任何人都可以加入。" "任何人" - "选择哪些无需邀请即可加入此房间的空间成员。%1$s" + "选择哪些空间的成员无需邀请即可加入本聊天室。%1$s" "管理空间" - "仅限受邀人员加入。" + "仅限受邀者加入。" "仅限受邀者" "访问权限" "任何位于已授权空间的成员均可加入。" "%1$s 中的任何人都可加入。" "空间成员" - "“空间”功能当前不受支持" - "你需要一个地址才能使其在公共目录中可见。" + "目前不支持空间" + "您需要一个地址才能在公共目录中显示。" "地址" "允许通过搜索 %1$s 的公共房间目录来发现此房间" "通过公共目录搜索功能实现可被发现性。" @@ -39,12 +39,12 @@ "任何人(历史记录公开)" "更改不会影响之前的消息,只会影响新消息。%1$s" "谁可以读取历史记录" - "自成员被邀请时起" + "自受邀以来的成员" "成员(完整历史记录)" "房间地址是查找和访问房间的方式。这也确保你可以轻松地向他人分享房间。 你可以选择在你服务器的公共房间目录中发布你的房间。" "房间发布" - "地址是查找和访问房间及空间的途径,同时确保你能轻松与他人共享。" + "地址是查找和访问聊天室及空间的途径,同时确保您能轻松与他人共享。" "可见性" "安全与隐私" 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 2c0a6c9acb..17d6f3a88d 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity @@ -26,82 +23,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 org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EditRoomAddressViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `click on back invokes expected callback`() = runAndroidComposeUiTest { + fun `click on back invokes expected callback`() { ensureCalledOnce { callback -> - setEditRoomAddressView(onBackClick = callback) - pressBack() + rule.setEditRoomAddressView(onBackClick = callback) + rule.pressBack() } } @Test - fun `click on disabled save doesn't emit event`() = runAndroidComposeUiTest { + fun `click on disabled save doesn't emit event`() { val recorder = EventsRecorder(expectEvents = false) val state = anEditRoomAddressState(eventSink = recorder) - setEditRoomAddressView(state) - clickOn(CommonStrings.action_save) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_save) recorder.assertEmpty() } @Test - fun `click on enabled save emits the expected event`() = runAndroidComposeUiTest { + fun `click on enabled save emits the expected event`() { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "room", roomAddressValidity = RoomAddressValidity.Valid, eventSink = recorder ) - setEditRoomAddressView(state) - clickOn(CommonStrings.action_save) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_save) recorder.assertSingle(EditRoomAddressEvents.Save) } @Test - fun `text changes on text field emits the expected event`() = runAndroidComposeUiTest { + fun `text changes on text field emits the expected event`() { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", eventSink = recorder ) - setEditRoomAddressView(state) + rule.setEditRoomAddressView(state) - onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") + rule.onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") recorder.assertSingle(EditRoomAddressEvents.RoomAddressChanged("alias")) } @Test - fun `click on dismiss error emits the expected event`() = runAndroidComposeUiTest { + fun `click on dismiss error emits the expected event`() { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", saveAction = AsyncAction.Failure(IllegalStateException()), eventSink = recorder ) - setEditRoomAddressView(state) - clickOn(CommonStrings.action_cancel) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_cancel) recorder.assertSingle(EditRoomAddressEvents.DismissError) } @Test - fun `click on retry error emits the expected event`() = runAndroidComposeUiTest { + fun `click on retry error emits the expected event`() { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", saveAction = AsyncAction.Failure(IllegalStateException()), eventSink = recorder ) - setEditRoomAddressView(state) - clickOn(CommonStrings.action_retry) + rule.setEditRoomAddressView(state) + rule.clickOn(CommonStrings.action_retry) recorder.assertSingle(EditRoomAddressEvents.Save) } } -private fun AndroidComposeUiTest.setEditRoomAddressView( +private fun AndroidComposeTestRule.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 de6da41823..c732df6df0 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.spaces.SpaceRoom @@ -27,22 +24,26 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBack 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() + @Test - fun `clicking back emits Cancel event`() = runAndroidComposeUiTest { + fun `clicking back emits Cancel event`() { val recorder = EventsRecorder() val state = aManageAuthorizedSpacesState(eventSink = recorder) - setManageAuthorizedSpacesView(state) - pressBack() + rule.setManageAuthorizedSpacesView(state) + rule.pressBack() recorder.assertSingle(ManageAuthorizedSpacesEvent.Cancel) } @Test - fun `clicking space checkbox emits ToggleSpace event`() = runAndroidComposeUiTest { + fun `clicking space checkbox emits ToggleSpace event`() { val roomId = A_ROOM_ID val space = aSpaceRoom(roomId = roomId, displayName = "Test Space") val recorder = EventsRecorder() @@ -50,37 +51,37 @@ class ManageAuthorizedSpacesViewTest { selectableSpaces = listOf(space), eventSink = recorder ) - setManageAuthorizedSpacesView(state) - onNodeWithText("Test Space").performClick() + rule.setManageAuthorizedSpacesView(state) + rule.onNodeWithText("Test Space").performClick() recorder.assertSingle(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) } @Test - fun `clicking done button emits Done event`() = runAndroidComposeUiTest { + fun `clicking done button emits Done event`() { val recorder = EventsRecorder() val state = aManageAuthorizedSpacesState( selectedIds = listOf(A_ROOM_ID), eventSink = recorder ) - setManageAuthorizedSpacesView(state) - clickOn(CommonStrings.action_done) + rule.setManageAuthorizedSpacesView(state) + rule.clickOn(CommonStrings.action_done) recorder.assertSingle(ManageAuthorizedSpacesEvent.Done) } @Test - fun `done button is disabled when no spaces selected`() = runAndroidComposeUiTest { + fun `done button is disabled when no spaces selected`() { val recorder = EventsRecorder(expectEvents = false) val state = aManageAuthorizedSpacesState( selectedIds = emptyList(), eventSink = recorder ) - setManageAuthorizedSpacesView(state) - clickOn(CommonStrings.action_done) + rule.setManageAuthorizedSpacesView(state) + rule.clickOn(CommonStrings.action_done) recorder.assertEmpty() } } -private fun AndroidComposeUiTest.setManageAuthorizedSpacesView( +private fun AndroidComposeTestRule.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 c46accbc91..a1f46b2938 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,16 +5,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.securityandprivacy.impl.R import io.element.android.libraries.architecture.AsyncAction @@ -26,69 +23,73 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBack 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() + @Test - fun `click on back invokes emits the expected event`() = runAndroidComposeUiTest { + fun `click on back invokes emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, ) - setSecurityAndPrivacyView(state) - pressBack() + rule.setSecurityAndPrivacyView(state) + rule.pressBack() recorder.assertSingle(SecurityAndPrivacyEvent.Exit) } @Test - fun `discard cancellation emits the expected event`() = runAndroidComposeUiTest { + fun `discard cancellation emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ) - setSecurityAndPrivacyView(state) - clickOn(CommonStrings.action_discard) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(CommonStrings.action_discard) recorder.assertSingle(SecurityAndPrivacyEvent.Exit) } @Test - fun `save cancellation confirmation emits the expected event`() = runAndroidComposeUiTest { + fun `save cancellation confirmation emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ) - setSecurityAndPrivacyView(state) - clickOn(CommonStrings.action_save, inDialog = true) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(CommonStrings.action_save, inDialog = true) recorder.assertSingle(SecurityAndPrivacyEvent.Save) } @Test - fun `click on room access item emits the expected event`() = runAndroidComposeUiTest { + fun `click on room access item emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, ) - setSecurityAndPrivacyView(state) - clickOn(R.string.screen_security_and_privacy_room_access_invite_only_option_title) + rule.setSecurityAndPrivacyView(state) + rule.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`() = runAndroidComposeUiTest { + fun `click on disabled save doesn't emit event`() { val recorder = EventsRecorder(expectEvents = false) val state = aSecurityAndPrivacyState(eventSink = recorder) - setSecurityAndPrivacyView(state) - clickOn(CommonStrings.action_save) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(CommonStrings.action_save) recorder.assertEmpty() } @Test - fun `click on enabled save emits the expected event`() = runAndroidComposeUiTest { + fun `click on enabled save emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -96,14 +97,14 @@ class SecurityAndPrivacyViewTest { roomAccess = SecurityAndPrivacyRoomAccess.Anyone, ) ) - setSecurityAndPrivacyView(state) - clickOn(CommonStrings.action_save) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(CommonStrings.action_save) recorder.assertSingle(SecurityAndPrivacyEvent.Save) } @Test @Config(qualifiers = "h640dp") - fun `click on room address item emits the expected event`() = runAndroidComposeUiTest { + fun `click on room address item emits the expected event`() { val address = "@alias:matrix.org" val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( @@ -113,14 +114,14 @@ class SecurityAndPrivacyViewTest { roomAccess = SecurityAndPrivacyRoomAccess.Anyone, ), ) - setSecurityAndPrivacyView(state) - onNodeWithText(address).performClick() + rule.setSecurityAndPrivacyView(state) + rule.onNodeWithText(address).performClick() recorder.assertSingle(SecurityAndPrivacyEvent.EditRoomAddress) } @Test @Config(qualifiers = "h1024dp") - fun `click on room visibility item emits the expected event`() = runAndroidComposeUiTest { + fun `click on room visibility item emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -129,14 +130,14 @@ class SecurityAndPrivacyViewTest { isVisibleInRoomDirectory = AsyncData.Success(false), ), ) - setSecurityAndPrivacyView(state) - clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title) + rule.setSecurityAndPrivacyView(state) + rule.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`() = runAndroidComposeUiTest { + fun `click on history visibility item emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -144,65 +145,65 @@ class SecurityAndPrivacyViewTest { historyVisibility = SecurityAndPrivacyHistoryVisibility.Invited, ), ) - setSecurityAndPrivacyView(state) - clickOn(R.string.screen_security_and_privacy_room_history_since_invite_option_title) + rule.setSecurityAndPrivacyView(state) + rule.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`() = runAndroidComposeUiTest { + fun `click on encryption item emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, savedSettings = aSecurityAndPrivacySettings(isEncrypted = false), ) - setSecurityAndPrivacyView(state) - clickOn(R.string.screen_security_and_privacy_encryption_toggle_title) + rule.setSecurityAndPrivacyView(state) + rule.clickOn(R.string.screen_security_and_privacy_encryption_toggle_title) recorder.assertSingle(SecurityAndPrivacyEvent.ToggleEncryptionState) } @Test - fun `click on encryption confirm emits the expected event`() = runAndroidComposeUiTest { + fun `click on encryption confirm emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, showEncryptionConfirmation = true, ) - setSecurityAndPrivacyView(state) - clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title) + rule.setSecurityAndPrivacyView(state) + rule.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`() = runAndroidComposeUiTest { + fun `click on space member access emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null), ) - setSecurityAndPrivacyView(state) - clickOn(R.string.screen_security_and_privacy_room_access_space_members_option_title) + rule.setSecurityAndPrivacyView(state) + rule.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`() = runAndroidComposeUiTest { + fun `click on ask to join with space members emits the expected event`() { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null), ) - setSecurityAndPrivacyView(state) - clickOn(R.string.screen_security_and_privacy_ask_to_join_option_title) + rule.setSecurityAndPrivacyView(state) + rule.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`() = runAndroidComposeUiTest { + fun `manage spaces footer is shown when space member access is selected`() { val recorder = EventsRecorder(expectEvents = false) val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -211,16 +212,15 @@ class SecurityAndPrivacyViewTest { roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(persistentListOf(A_ROOM_ID)), ), ) - setSecurityAndPrivacyView(state) + rule.setSecurityAndPrivacyView(state) // The footer text uses AnnotatedString with a link. Verify the footer text is displayed. - 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() + 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() } } -private fun AndroidComposeUiTest.setSecurityAndPrivacyView( +private fun AndroidComposeTestRule.setSecurityAndPrivacyView( state: SecurityAndPrivacyState = aSecurityAndPrivacyState( eventSink = EventsRecorder(expectEvents = false), ), diff --git a/features/signedout/impl/build.gradle.kts b/features/signedout/impl/build.gradle.kts index b3801288be..3c8aac5e25 100644 --- a/features/signedout/impl/build.gradle.kts +++ b/features/signedout/impl/build.gradle.kts @@ -27,7 +27,6 @@ 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 95a57db93e..396339adbb 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", - oAuthData = null, + oidcData = 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 deleted file mode 100644 index 6cba810df9..0000000000 --- a/features/signedout/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - "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-zh/translations.xml b/features/signedout/impl/src/main/res/values-zh/translations.xml index 6768608294..87c7620d98 100644 --- a/features/signedout/impl/src/main/res/values-zh/translations.xml +++ b/features/signedout/impl/src/main/res/values-zh/translations.xml @@ -1,8 +1,8 @@ "你在另一个会话中更改了密码" - "你已从其它会话中删除此会话" - "服务器管理员已禁止你的访问" - "你可能因以下原因而被注销。请重新登录以继续使用 %s。" - "你已注销" + "你已从其他会话中删除本会话" + "您的服务器管理员已禁止您访问" + "您可能因下列原因而被登出。请重新登录以继续使用 %s。" + "你已登出" 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 882db26f00..10c70d6f7d 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,6 +15,7 @@ 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/leave/LeaveSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt index e0876c255c..4e1fd34673 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.isDm != true } + val otherRoomsExcludingDm = otherRooms.filter { it.spaceRoom.isDirect != 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 ddf34cb076..3d8df360a7 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,7 +11,6 @@ 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 @@ -118,7 +117,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 6824cc6c10..c43257b383 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,7 +109,6 @@ private fun aSpaceInfo( avatarUrl = null, isPublic = true, isDirect = false, - isDm = false, isEncrypted = false, joinRule = joinRule, isSpace = true, @@ -140,7 +139,6 @@ 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 29898a1f63..05fb75ee5e 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,8 +354,7 @@ 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( @@ -426,7 +425,6 @@ private fun SpaceViewTopBar( modifier = Modifier .clip(roundedCornerShape) .clickable(enabled = canAccessSpaceSettings, onClick = onSettingsClick) - .semantics { heading() } ) }, actions = { @@ -534,7 +532,6 @@ private fun ManageModeTopBar( Text( text = pluralStringResource(CommonPlurals.common_selected_count, selectedCount, selectedCount), style = ElementTheme.typography.fontBodyLgMedium, - modifier = Modifier.semantics { heading() }, ) }, actions = { @@ -588,7 +585,10 @@ private fun SpaceAvatarAndNameRow( ) Text( modifier = Modifier - .padding(horizontal = 8.dp), + .padding(horizontal = 8.dp) + .semantics { + heading() + }, 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 36f6a2a1d0..2030b6885a 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,7 +9,6 @@ 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 @@ -25,7 +24,7 @@ open class SpaceSettingsStateProvider : PreviewParameterProvider - - "Rols i permisos" - "Seguretat i privadesa" - diff --git a/features/space/impl/src/main/res/values-de/translations.xml b/features/space/impl/src/main/res/values-de/translations.xml index 6dee82123f..bc1e4265b6 100644 --- a/features/space/impl/src/main/res/values-de/translations.xml +++ b/features/space/impl/src/main/res/values-de/translations.xml @@ -8,11 +8,9 @@ "Dadurch wirst du auch aus allen Chats in diesem Space entfernt." "Du musst einen anderen Admin für diesen Space zuweisen, bevor du ihn verlassen kannst." - "Du bist der einzige Eigentümer von %1$s. Du musst die Eigentumsrechte an jemand anderen übertragen, bevor du den Space verlässt." "Du wirst aus den folgenden Chats nicht entfernt, weil du der einzige Admin bist:" "%1$s verlassen?" "Du bist der einzige Administrator für %1$s" - "Eigentumsrechte übertragen" "Chat" "Das Hinzufügen eines Chats hat keinen Einfluss auf die Beitrittsregeln. Um die Regeln zu ändern, gehe zu \"Raum Info\" und dann zu \"Datenschutz und Sicherheit\"" "Füge deinen ersten Chat hinzu" 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 d8f377c0b2..dcab5c912a 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-hr/translations.xml b/features/space/impl/src/main/res/values-hr/translations.xml index d1c6ba60a2..91731dbe71 100644 --- a/features/space/impl/src/main/res/values-hr/translations.xml +++ b/features/space/impl/src/main/res/values-hr/translations.xml @@ -9,20 +9,10 @@ "Odaberite sobe koje želite napustiti, a za koje niste jedini administrator:" "Morate dodijeliti drugog administratora za ovaj prostor prije nego što ga napustite." - "Vi ste jedini vlasnik %1$s . Prije odlaska morate prenijeti vlasništvo na nekog drugog." "Nećete biti uklonjeni iz sljedećih soba jer ste jedini administrator:" "Želite li napustiti %1$s?" "Vi ste jedini administrator za %1$s" - "Prenesi vlasništvo" - "Soba" - "Dodavanje sobe neće utjecati na pristup sobi. Za promjenu pristupa idite na Postavke sobe > Sigurnost i privatnost." - "sobu" "Prikaži članove" - "Uklanjanje sobe neće utjecati na pristup sobi. Za promjenu pristupa idite na Informacije o sobi > Privatnost i sigurnost." - - "Uklonite %1$d soba od %2$s" - "Uklonite %1$d sobe od %2$s" - "Napusti prostor" "Uloge i dopuštenja" "Sigurnost i privatnost" 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 9c3097b75f..f1a964c6e3 100644 --- a/features/space/impl/src/main/res/values-pl/translations.xml +++ b/features/space/impl/src/main/res/values-pl/translations.xml @@ -9,21 +9,9 @@ "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 7748f44ee2..2bf5d0e251 100644 --- a/features/space/impl/src/main/res/values-ro/translations.xml +++ b/features/space/impl/src/main/res/values-ro/translations.xml @@ -9,21 +9,10 @@ "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 df4b4fb922..e124f72826 100644 --- a/features/space/impl/src/main/res/values-uk/translations.xml +++ b/features/space/impl/src/main/res/values-uk/translations.xml @@ -2,28 +2,14 @@ "Оберіть власників" "%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-uz/translations.xml b/features/space/impl/src/main/res/values-uz/translations.xml index 3a924aa7ae..ae0bee0a51 100644 --- a/features/space/impl/src/main/res/values-uz/translations.xml +++ b/features/space/impl/src/main/res/values-uz/translations.xml @@ -8,20 +8,10 @@ "Siz yagona administrator bo‘lmagan xonalardan chiqishni xohlasangiz, ularni tanlang:" " Ketishingizdan oldin bu maydon uchun boshqa administrator tayinlashingiz kerak." - "Siz %1$s yagona egasisiz. Ketishdan oldin egalik huquqini boshqa shaxsga o‘tkazishingiz kerak." "Siz quyidagi xona(lar)dan olib tashlanmaysiz, chunki siz yagona administratorsiz:" "%1$s dan chiqasizmi?" "Siz %1$s uchun yagona administratorsiz" - "Egalikni topshirish" - "Xona" - "Xona kiritish xonaga kirishga ta’sir qilmaydi. Ruxsatni o‘zgartirish uchun Xona sozlamalari > Xavfsizlik va maxfiylik rukniga kiring." - "Birinchi xonangizni qo‘shing" "A’zolarni ko‘rish" - "Xona olib tashlansa, unga kirish ruxsatiga ta’sir qilmaydi. Ruxsatni o‘zgartirish uchun Xona haqida > Maxfiylik va xavfsizlik rukniga kiring." - - "%1$d ta xonani %2$sdan olib tashlash" - "%1$d ta xonani %2$sdan olib tashlash" - "Maydondan chiqish" "Rollar va ruxsatlar" "Xavfsizlik va maxfiylik" diff --git a/features/space/impl/src/main/res/values-vi/translations.xml b/features/space/impl/src/main/res/values-vi/translations.xml index f8c0f5b6c5..a19747d029 100644 --- a/features/space/impl/src/main/res/values-vi/translations.xml +++ b/features/space/impl/src/main/res/values-vi/translations.xml @@ -1,12 +1,5 @@ - "Chọn chủ sở hữu" - - "Rời khỏi %1$d phòng và không gian" - - - "Xoá %1$d phòng từ %2$s" - "Rời space" "Vai trò và quyền hạn" diff --git a/features/space/impl/src/main/res/values-zh-rTW/translations.xml b/features/space/impl/src/main/res/values-zh-rTW/translations.xml index 623f30923e..f2be9a1875 100644 --- a/features/space/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/space/impl/src/main/res/values-zh-rTW/translations.xml @@ -7,19 +7,10 @@ "這也會將您從此空間中的所有聊天室移除。" "您必須為此空間另外指定一位管理員後才能離開。" - "您是 %1$s 唯一的擁有者。在您離開前,您必須將所有權轉移給其他人。" "您不會被從以下聊天室移除,因為您是唯一的管理員:" "離開 %1$s?" "您是 %1$s 唯一的管理員" - "轉移所有權" - "聊天室" - "新增聊天室不會影響聊天室存取權。要變更存取權,請前往「聊天室設定」→「安全性與隱私權」" - "新增您的第一個聊天室" "檢視成員" - "移除聊天室不會影響聊天室存取權。要變更存取權,請前往「聊天室資訊」→「隱私權與安全性」。" - - "從 %2$s 移除 %1$d 個聊天室" - "離開空間" "角色與權限" "安全與隱私" 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 cbe9d54c32..cdac25a8ae 100644 --- a/features/space/impl/src/main/res/values-zh/translations.xml +++ b/features/space/impl/src/main/res/values-zh/translations.xml @@ -1,24 +1,24 @@ "选择所有者" - "%1$s(管理员)" + "%1$s (管理员)" "离开 %1$d 个房间和空间" - "选择想要退出并且你不是其唯一管理员的房间:" - "你需要为该空间指定另一位管理员才能离开。" - "你是 %1$s 的唯一所有者。在离开前需要将所有权转移给他人。" - "由于因为你是唯一的管理员,你不会从以下房间被移除:" - "离开 %1$s?" - "你是 %1$s 中唯一的管理员" + "选择您想要离开且您不是其唯一管理员的房间:" + "您需要为该空间指定另一位管理员才能离开。" + "您是%1$s 的唯一所有者。在您离开前,需要将所有权转移给他人。" + "您不会从以下房间中被移除,因为您是唯一的管理员:" + "离开%1$s?" + "您是 %1$s 的唯一管理员" "转让所有权" - "房间" - "添加房间不会影响其访问权限。如需更改访问权限,请前往“房间设置” > “安全与隐私”。" - "添加第一个房间" + "聊天室" + "添加聊天室不会影响其访问权限。如需更改访问权限,请前往“聊天室设置” > “安全与隐私”。" + "添加您的第一个聊天室" "查看成员" - "移除房间不会影响其访问权限。要更改访问权限,请转到“房间信息” > “隐私和安全”。" + "移除聊天室不会影响其访问权限。要更改访问权限,请转到“聊天室信息”>“隐私和安全”。" - "移除 %2$s 个房间,共 %1$d 个" + "移除 %1$d 个 %2$s 中的聊天室" "离开空间" "角色与权限" 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 6fc10f1e82..d75fecd05a 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,16 +5,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState @@ -25,73 +22,77 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack 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() + @Test - fun `clicking back when search inactive emits Dismiss and invokes onBackClick`() = runAndroidComposeUiTest { + fun `clicking back when search inactive emits Dismiss and invokes onBackClick`() { val eventsRecorder = EventsRecorder() ensureCalledOnce { - setAddRoomToSpaceView( + rule.setAddRoomToSpaceView( anAddRoomToSpaceState( isSearchActive = false, eventSink = eventsRecorder, ), onBackClick = it, ) - pressBack() + rule.pressBack() } eventsRecorder.assertSingle(AddRoomToSpaceEvent.Dismiss) } @Test - fun `clicking back when search active emits CloseSearch event`() = runAndroidComposeUiTest { + fun `clicking back when search active emits CloseSearch event`() { val eventsRecorder = EventsRecorder() - setAddRoomToSpaceView( + rule.setAddRoomToSpaceView( anAddRoomToSpaceState( isSearchActive = true, eventSink = eventsRecorder, ), ) - pressBack() + rule.pressBack() eventsRecorder.assertSingle(AddRoomToSpaceEvent.OnSearchActiveChanged(false)) } @Test - fun `clicking save emits Save event`() = runAndroidComposeUiTest { + fun `clicking save emits Save event`() { val eventsRecorder = EventsRecorder() - setAddRoomToSpaceView( + rule.setAddRoomToSpaceView( anAddRoomToSpaceState( selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_save) + rule.clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(AddRoomToSpaceEvent.Save) } @Config(qualifiers = "h1024dp") @Test - fun `clicking room in suggestions emits ToggleRoom event`() = runAndroidComposeUiTest { + fun `clicking room in suggestions emits ToggleRoom event`() { val suggestions = aSelectRoomInfoList() val eventsRecorder = EventsRecorder() - setAddRoomToSpaceView( + rule.setAddRoomToSpaceView( anAddRoomToSpaceState( suggestions = suggestions, eventSink = eventsRecorder, ), ) - onNodeWithText(suggestions.first().name!!).performClick() + rule.onNodeWithText(suggestions.first().name!!).performClick() eventsRecorder.assertSingle(AddRoomToSpaceEvent.ToggleRoom(suggestions.first())) } @Test - fun `onRoomsAdded called when saveAction is Success`() = runAndroidComposeUiTest { + fun `onRoomsAdded called when saveAction is Success`() { ensureCalledOnce { - setAddRoomToSpaceView( + rule.setAddRoomToSpaceView( anAddRoomToSpaceState( saveAction = AsyncAction.Success(Unit), ), @@ -102,10 +103,10 @@ class AddRoomToSpaceViewTest { @Config(qualifiers = "h1024dp") @Test - fun `displaying search results sends UpdateSearchVisibleRange event`() = runAndroidComposeUiTest { + fun `displaying search results sends UpdateSearchVisibleRange event`() { val eventsRecorder = EventsRecorder() val rooms = aSelectRoomInfoList() - setAddRoomToSpaceView( + rule.setAddRoomToSpaceView( anAddRoomToSpaceState( isSearchActive = true, searchResults = SearchBarResultState.Results(rooms), @@ -116,7 +117,7 @@ class AddRoomToSpaceViewTest { } } -private fun AndroidComposeUiTest.setAddRoomToSpaceView( +private fun AndroidComposeTestRule.setAddRoomToSpaceView( state: AddRoomToSpaceState, onBackClick: () -> Unit = EnsureNeverCalled(), onRoomsAdded: () -> Unit = EnsureNeverCalled(), 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 021f52defd..b3b6fc7976 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, isDm = false) + spaceRoom = aSpaceRoom(roomId = A_ROOM_ID, isDirect = false) ), aLeaveSpaceRoom( - spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_2, isDm = true) + spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_2, isDirect = true) ), aLeaveSpaceRoom( - spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_3, isDm = null) + spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_3, isDirect = 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 6632c7f4f8..87343b6e34 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,17 +6,14 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.CurrentUserMembership @@ -36,33 +33,37 @@ 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 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() + @Test - fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setSpaceView( + rule.setSpaceView( aSpaceState( hasMoreToLoad = false, eventSink = eventsRecorder, ), onBackClick = it, ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on a room name invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on a room name invokes the expected callback`() { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(aSpaceRoom) { - setSpaceView( + rule.setSpaceView( aSpaceState( children = listOf(aSpaceRoom), hasMoreToLoad = false, @@ -70,91 +71,91 @@ class SpaceViewTest { ), onRoomClick = it, ) - onNodeWithText(A_ROOM_NAME).performClick() + rule.onNodeWithText(A_ROOM_NAME).performClick() } } @Test - fun `clicking on Join room emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on Join room emits the expected Event`() { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = null) val eventsRecorder = EventsRecorder() - setSpaceView( + rule.setSpaceView( aSpaceState( children = listOf(aSpaceRoom), hasMoreToLoad = false, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_join) + rule.clickOn(CommonStrings.action_join) eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on accept invite emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on accept invite emits the expected Event`() { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) val eventsRecorder = EventsRecorder() - setSpaceView( + rule.setSpaceView( aSpaceState( hasMoreToLoad = false, children = listOf(aSpaceRoom), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_accept) + rule.clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on decline invite emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on decline invite emits the expected Event`() { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) val eventsRecorder = EventsRecorder() - setSpaceView( + rule.setSpaceView( aSpaceState( hasMoreToLoad = false, children = listOf(aSpaceRoom), eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_decline) + rule.clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on topic emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on topic emits the expected Event`() { val eventsRecorder = EventsRecorder() - setSpaceView( + rule.setSpaceView( aSpaceState( spaceInfo = aRoomInfo(topic = A_ROOM_TOPIC), hasMoreToLoad = false, eventSink = eventsRecorder, ) ) - onNodeWithText(A_ROOM_TOPIC).performClick() + rule.onNodeWithText(A_ROOM_TOPIC).performClick() eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC)) } @Test - fun `clicking back in manage mode emits ExitManageMode event`() = runAndroidComposeUiTest { + fun `clicking back in manage mode emits ExitManageMode event`() { val eventsRecorder = EventsRecorder() - setSpaceView( + rule.setSpaceView( aSpaceState( hasMoreToLoad = false, isManageMode = true, eventSink = eventsRecorder, ) ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(SpaceEvents.ExitManageMode) } @Test - fun `clicking on room in manage mode emits ToggleRoomSelection event`() = runAndroidComposeUiTest { + fun `clicking on room in manage mode emits ToggleRoomSelection event`() { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) val eventsRecorder = EventsRecorder() - setSpaceView( + rule.setSpaceView( aSpaceState( children = listOf(aSpaceRoom), hasMoreToLoad = false, @@ -162,14 +163,14 @@ class SpaceViewTest { eventSink = eventsRecorder, ) ) - onNodeWithText(A_ROOM_NAME).performClick() + rule.onNodeWithText(A_ROOM_NAME).performClick() eventsRecorder.assertSingle(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) } @Test - fun `clicking remove button emits RemoveSelectedRooms event`() = runAndroidComposeUiTest { + fun `clicking remove button emits RemoveSelectedRooms event`() { val eventsRecorder = EventsRecorder() - setSpaceView( + rule.setSpaceView( aSpaceState( children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), hasMoreToLoad = false, @@ -178,15 +179,15 @@ class SpaceViewTest { eventSink = eventsRecorder, ) ) - clickOn(CommonStrings.action_remove) + rule.clickOn(CommonStrings.action_remove) eventsRecorder.assertSingle(SpaceEvents.RemoveSelectedRooms) } @Config(qualifiers = "h1024dp") @Test - fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() = runAndroidComposeUiTest { + fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() { val eventsRecorder = EventsRecorder() - setSpaceView( + rule.setSpaceView( aSpaceState( children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), hasMoreToLoad = false, @@ -197,14 +198,14 @@ class SpaceViewTest { ) ) // Click on the Remove button in the confirmation dialog - clickOn(CommonStrings.action_remove, inDialog = true) + rule.clickOn(CommonStrings.action_remove, inDialog = true) eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval) } @Test - fun `clicking create room button calls the expected callback`() = runAndroidComposeUiTest { + fun `clicking create room button calls the expected callback`() { val onCreateRoomClick = lambdaRecorder { } - setSpaceView( + rule.setSpaceView( aSpaceState( children = emptyList(), hasMoreToLoad = false, @@ -213,14 +214,14 @@ class SpaceViewTest { ), onCreateRoomClick = onCreateRoomClick, ) - clickOn(CommonStrings.action_create_room) + rule.clickOn(CommonStrings.action_create_room) onCreateRoomClick.assertions().isCalledOnce() } @Test - fun `clicking add existing room button calls the expected callback`() = runAndroidComposeUiTest { + fun `clicking add existing room button calls the expected callback`() { val onAddRoomClick = lambdaRecorder { } - setSpaceView( + rule.setSpaceView( aSpaceState( children = emptyList(), hasMoreToLoad = false, @@ -229,12 +230,12 @@ class SpaceViewTest { ), onAddRoomClick = onAddRoomClick, ) - clickOn(CommonStrings.action_add_existing_rooms) + rule.clickOn(CommonStrings.action_add_existing_rooms) onAddRoomClick.assertions().isCalledOnce() } } -private fun AndroidComposeUiTest.setSpaceView( +private fun AndroidComposeTestRule.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/DefaultStartDMAction.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt index 6821005c67..3bfbd1ca18 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt @@ -15,6 +15,8 @@ import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.startchat.api.StartDMAction import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.StartDMResult @@ -26,6 +28,7 @@ import io.element.android.services.analytics.api.AnalyticsService class DefaultStartDMAction( private val matrixClient: MatrixClient, private val analyticsService: AnalyticsService, + private val featureFlagService: FeatureFlagService, ) : StartDMAction { override suspend fun execute( matrixUser: MatrixUser, @@ -47,7 +50,7 @@ class DefaultStartDMAction( val identityState = matrixClient.encryptionService.getUserIdentity(matrixUser.userId, fallbackToServer = false).getOrNull() actionState.value = ConfirmingStartDmWithMatrixUser( matrixUser = matrixUser, - isUserIdentityUnknown = identityState == null + isUserIdentityUnknown = featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite) && identityState == null ) } } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt index 360c59881c..e4f8a4faf2 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt @@ -13,10 +13,8 @@ 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.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -54,13 +52,11 @@ fun JoinRoomByAddressView( onDismissRequest = { state.eventSink(JoinRoomByAddressEvent.Dismiss) }, - scrollable = false, ) { Column( modifier = Modifier .fillMaxWidth() - .padding(all = 16.dp) - .verticalScroll(rememberScrollState()), + .padding(all = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { RoomAddressField( 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 079ece4180..7afbe19c3d 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt @@ -10,6 +10,8 @@ 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 @@ -22,6 +24,8 @@ 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 @@ -33,6 +37,7 @@ class StartChatPresenter( userListDataStore: UserListDataStore, private val startDMAction: StartDMAction, private val buildMeta: BuildMeta, + private val featureFlagService: FeatureFlagService, ) : Presenter { private val presenter = presenterFactory.create( UserListPresenterArgs( @@ -49,6 +54,12 @@ class StartChatPresenter( val localCoroutineScope = rememberCoroutineScope() val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } + val isRoomDirectorySearchEnabled by remember { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch) + }.collectAsState(initial = false) + + val enableKeyShareOnInvite = featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(false) + fun handleEvent(event: StartChatEvents) { when (event) { is StartChatEvents.StartDM -> localCoroutineScope.launch { @@ -66,6 +77,8 @@ class StartChatPresenter( applicationName = buildMeta.applicationName, userListState = userListState, startDmAction = startDmActionState.value, + isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, + enableKeyShareOnInvite = enableKeyShareOnInvite.value, eventSink = ::handleEvent, ) } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt index e6746e1302..989a5b8d20 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt @@ -16,5 +16,7 @@ data class StartChatState( val applicationName: String, val userListState: UserListState, val startDmAction: AsyncAction, + val isRoomDirectorySearchEnabled: Boolean, + val enableKeyShareOnInvite: Boolean, val eventSink: (StartChatEvents) -> Unit, ) diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt index 41a3551d1d..17d83a9e11 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt @@ -55,6 +55,9 @@ open class StartChatStateProvider : PreviewParameterProvider { aCreateRoomRootState( startDmAction = aConfirmingStartDmWithMatrixUser() ), + aCreateRoomRootState( + isRoomDirectorySearchEnabled = true, + ), ) } @@ -72,10 +75,13 @@ 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, + enableKeyShareOnInvite = false, eventSink = eventSink, ) diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt index 7537fee524..28bf52549e 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt @@ -130,6 +130,7 @@ fun StartChatView( if (data is ConfirmingStartDmWithMatrixUser) { CreateDmConfirmationBottomSheet( matrixUser = data.matrixUser, + enableKeyShareOnInvite = state.enableKeyShareOnInvite, isUserIdentityUnknown = data.isUserIdentityUnknown, onSendInvite = { state.eventSink(StartChatEvents.StartDM(data.matrixUser)) @@ -176,12 +177,14 @@ private fun CreateRoomActionButtonsList( onClick = onNewRoomClick, ) } - item { - CreateRoomActionButton( - iconRes = CompoundDrawables.ic_compound_list_bulleted, - text = stringResource(id = R.string.screen_room_directory_search_title), - onClick = onRoomDirectorySearchClick, - ) + 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( diff --git a/features/startchat/impl/src/main/res/values-ca/translations.xml b/features/startchat/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index e249d61582..0000000000 --- a/features/startchat/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - "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/main/res/values-zh/translations.xml b/features/startchat/impl/src/main/res/values-zh/translations.xml index 0467ab9c0e..fcbb6afd45 100644 --- a/features/startchat/impl/src/main/res/values-zh/translations.xml +++ b/features/startchat/impl/src/main/res/values-zh/translations.xml @@ -1,7 +1,7 @@ - "新房间" - "房间目录" + "新聊天室" + "聊天室目录" "在开始聊天时发生了错误" "输入地址加入房间" "地址无效" diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt index 2c1fd1aa2b..88b935e47d 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt @@ -13,6 +13,9 @@ import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.CreatedRoom import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -85,7 +88,7 @@ class DefaultStartDMActionTest { val state = mutableStateOf>(AsyncAction.Uninitialized) val matrixUser = aMatrixUser() action.execute(matrixUser, false, state) - assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = true)) + assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false)) assertThat(analyticsService.capturedEvents).isEmpty() } @@ -104,31 +107,37 @@ class DefaultStartDMActionTest { } @Test - fun `when user identity fetched and identity unknown`() = runTest { + fun `when history sharing enabled, user identity fetched and identity unknown`() = runTest { val getUserIdentityResult = lambdaRecorder> { _ -> Result.success(null) } val encryptionService = FakeEncryptionService(getUserIdentityResult = getUserIdentityResult) val matrixClient = FakeMatrixClient(encryptionService = encryptionService).apply { givenFindDmResult(Result.success(null)) } + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } val action = createStartDMAction( matrixClient = matrixClient, + featureFlagService = featureFlagService ) val state = mutableStateOf>(AsyncAction.Uninitialized) action.execute(aMatrixUser(), false, state) - getUserIdentityResult.assertions().isCalledOnce() + assertThat(getUserIdentityResult.assertions().isCalledOnce()) assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(aMatrixUser(), isUserIdentityUnknown = true)) } private fun createStartDMAction( matrixClient: MatrixClient = FakeMatrixClient(), analyticsService: AnalyticsService = FakeAnalyticsService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService() ): DefaultStartDMAction { return DefaultStartDMAction( matrixClient = matrixClient, analyticsService = analyticsService, + featureFlagService = featureFlagService, ) } } diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt index dd992a9d2f..92162ca82c 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,54 +6,56 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.performTextInput -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 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.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() + @Test - fun `entering text emits the expected event`() = runAndroidComposeUiTest { + fun `entering text emits the expected event`() { val eventsRecorder = EventsRecorder() - setJoinRoomByAddressView( + rule.setJoinRoomByAddressView( aJoinRoomByAddressState( eventSink = eventsRecorder, ) ) - val text = activity!!.getString(R.string.screen_start_chat_join_room_by_address_action) - onNodeWithText(text).performTextInput("#address:matrix.org") + val text = rule.activity.getString(R.string.screen_start_chat_join_room_by_address_action) + rule.onNodeWithText(text).performTextInput("#address:matrix.org") eventsRecorder.assertSingle(JoinRoomByAddressEvent.UpdateAddress("#address:matrix.org")) } @Test - fun `clicking on continue emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on continue emits the expected event`() { val eventsRecorder = EventsRecorder() - setJoinRoomByAddressView( + rule.setJoinRoomByAddressView( aJoinRoomByAddressState( eventSink = eventsRecorder, ) ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(JoinRoomByAddressEvent.Continue) } } -private fun AndroidComposeUiTest.setJoinRoomByAddressView( +private fun AndroidComposeTestRule.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 57f6cd2333..2bc15e989d 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt @@ -17,6 +17,8 @@ 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 @@ -153,16 +155,34 @@ 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 916611bf20..9237f3433c 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,16 +6,13 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.startchat.impl.R import io.element.android.features.startchat.impl.userlist.aRecentDirectRoomList @@ -30,65 +27,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 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() + @Test - fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on back invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setStartChatView( + rule.setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, ), onCloseClick = it ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on New room invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on New room invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setStartChatView( + rule.setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, ), onNewRoomClick = it ) - clickOn(R.string.screen_create_room_action_create_room) + rule.clickOn(R.string.screen_create_room_action_create_room) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Invite people invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on Invite people invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setStartChatView( + rule.setStartChatView( aCreateRoomRootState( applicationName = "test", eventSink = eventsRecorder, ), onInviteFriendsClick = it ) - val text = activity!!.getString(CommonStrings.action_invite_friends_to_app, "test") - onNodeWithText(text).performClick() + val text = rule.activity.getString(CommonStrings.action_invite_friends_to_app, "test") + rule.onNodeWithText(text).performClick() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on a user suggestion invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on a user suggestion invokes the expected callback`() { val recentDirectRoomList = aRecentDirectRoomList() val firstRoom = recentDirectRoomList[0] val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(firstRoom.roomId) { - setStartChatView( + rule.setStartChatView( aCreateRoomRootState( userListState = aUserListState( recentDirectRooms = recentDirectRoomList @@ -97,41 +99,42 @@ class StartChatViewTest { ), onOpenDM = it ) - onNodeWithText(firstRoom.matrixUser.getBestName()).performClick() + rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Join room by address invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on Join room by address invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setStartChatView( + rule.setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, ), onJoinRoomByAddressClick = it ) - clickOn(R.string.screen_start_chat_join_room_by_address_action) + rule.clickOn(R.string.screen_start_chat_join_room_by_address_action) } } @Test - fun `clicking on room directory invokes the expected callback`() = runAndroidComposeUiTest { + fun `clicking on room directory invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setStartChatView( + rule.setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, + isRoomDirectorySearchEnabled = true ), onRoomDirectorySearchClick = it ) - clickOn(R.string.screen_room_directory_search_title) + rule.clickOn(R.string.screen_room_directory_search_title) } } } -private fun AndroidComposeUiTest.setStartChatView( +private fun AndroidComposeTestRule.setStartChatView( state: StartChatState, onCloseClick: () -> Unit = EnsureNeverCalled(), onNewRoomClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt index 0e0016ee14..e2a309c17f 100644 --- a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt @@ -26,6 +26,7 @@ data class UserProfileState( val dmRoomId: RoomId?, val canCall: Boolean, val snackbarMessage: SnackbarMessage?, + val enableKeyShareOnInvite: Boolean, val eventSink: (UserProfileEvents) -> Unit ) { enum class ConfirmationDialog { diff --git a/features/userprofile/impl/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 aff9e3502d..aaafbe04be 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.CallData +import io.element.android.features.call.api.CallType 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( - CallData( + CallType.RoomCall( sessionId = sessionId, roomId = dmRoomId, isAudioCall = callIntent == CallIntent.AUDIO diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt index 7e09a03ec3..a451d86b70 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState @@ -31,6 +32,8 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -50,6 +53,7 @@ class UserProfilePresenter( private val client: MatrixClient, private val startDMAction: StartDMAction, private val sessionEnterpriseService: SessionEnterpriseService, + private val featureFlagService: FeatureFlagService, ) : Presenter { @AssistedFactory interface Factory { @@ -101,6 +105,8 @@ class UserProfilePresenter( } val userProfile by produceState(null) { value = client.getProfile(userId).getOrNull() } + val enableKeyShareOnInvite = featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(false) + fun handleEvent(event: UserProfileEvents) { when (event) { is UserProfileEvents.BlockUser -> { @@ -153,6 +159,7 @@ class UserProfilePresenter( dmRoomId = dmRoomId, canCall = canCall, snackbarMessage = null, + enableKeyShareOnInvite = enableKeyShareOnInvite.value, eventSink = ::handleEvent, ) } diff --git a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt index bbdc698f17..1325b46bc0 100644 --- a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt +++ b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.impl.root.UserProfilePresenter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -414,6 +415,7 @@ class UserProfilePresenterTest { sessionEnterpriseService = FakeSessionEnterpriseService( isElementCallAvailableResult = { isElementCallAvailable }, ), + featureFlagService = FakeFeatureFlagService() ) } } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt index 5ba0280b14..ac146a36a7 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,7 +37,6 @@ 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 @@ -141,7 +140,7 @@ internal fun UserProfileHeaderSectionPreview() = ElementPreview { UserProfileHeaderSection( avatarUrl = null, userId = UserId("@alice:example.com"), - userName = USER_NAME_ALICE, + userName = "Alice", verificationState = UserProfileVerificationState.VERIFIED, openAvatarPreview = {}, onUserIdClick = {}, @@ -155,7 +154,7 @@ internal fun UserProfileHeaderSectionWithVerificationViolationPreview() = Elemen UserProfileHeaderSection( avatarUrl = null, userId = UserId("@alice:example.com"), - userName = USER_NAME_ALICE, + userName = "Alice", verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION, openAvatarPreview = {}, onUserIdClick = {}, diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt index fe318a8670..a4bbcd6aa4 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt @@ -61,5 +61,6 @@ fun aUserProfileState( dmRoomId = dmRoomId, canCall = canCall, snackbarMessage = snackbarMessage, + enableKeyShareOnInvite = false, eventSink = eventSink, ) diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt index 5d541edf2b..34f992f77d 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt @@ -114,6 +114,7 @@ fun UserProfileView( if (data is ConfirmingStartDmWithMatrixUser) { CreateDmConfirmationBottomSheet( matrixUser = data.matrixUser, + enableKeyShareOnInvite = state.enableKeyShareOnInvite, isUserIdentityUnknown = data.isUserIdentityUnknown, onSendInvite = { state.eventSink(UserProfileEvents.StartDM) diff --git a/features/userprofile/shared/src/main/res/values-ca/translations.xml b/features/userprofile/shared/src/main/res/values-ca/translations.xml deleted file mode 100644 index bacb920a58..0000000000 --- a/features/userprofile/shared/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - "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/main/res/values-zh/translations.xml b/features/userprofile/shared/src/main/res/values-zh/translations.xml index fd1906725d..38c4e3e8eb 100644 --- a/features/userprofile/shared/src/main/res/values-zh/translations.xml +++ b/features/userprofile/shared/src/main/res/values-zh/translations.xml @@ -13,7 +13,7 @@ "解除屏蔽" "可以重新接收他们的消息。" "解除屏蔽用户" - "使用 Web 客户端验证此用户。" + "使用 Web 应用程序验证此用户。" "验证 %1$s" "在开始聊天时发生了错误" 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 b1d81f374c..83b10e2a53 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,16 +6,13 @@ * 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState @@ -42,188 +39,193 @@ 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 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() + @Test - fun `on back button click - the expected callback is called`() = runAndroidComposeUiTest { + fun `on back button click - the expected callback is called`() = runTest { ensureCalledOnce { callback -> - setUserProfileView( + rule.setUserProfileView( goBack = callback, ) - pressBack() + rule.pressBack() } } @Test - fun `on avatar clicked - the expected callback is called`() = runAndroidComposeUiTest { + fun `on avatar clicked - the expected callback is called`() = runTest { ensureCalledOnceWithTwoParams(A_USER_NAME, AN_AVATAR_URL) { callback -> - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState(userName = A_USER_NAME, avatarUrl = AN_AVATAR_URL), openAvatarPreview = callback, ) - onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() + rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() } } @Test - fun `on avatar clicked with no avatar - nothing happens`() = runAndroidComposeUiTest { + fun `on avatar clicked with no avatar - nothing happens`() = runTest { val callback = EnsureNeverCalledWithTwoParams() - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState(userName = A_USER_NAME, avatarUrl = null), openAvatarPreview = callback, ) - onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() + rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() } @Test - fun `on Share clicked - the expected callback is called`() = runAndroidComposeUiTest { + fun `on Share clicked - the expected callback is called`() = runTest { ensureCalledOnce { callback -> - setUserProfileView( + rule.setUserProfileView( onShareUser = callback, ) - clickOn(CommonStrings.action_share) + rule.clickOn(CommonStrings.action_share) } } @Test - fun `on Message clicked - the StartDm event is emitted`() = runAndroidComposeUiTest { + fun `on Message clicked - the StartDm event is emitted`() = runTest { val eventsRecorder = EventsRecorder() - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState( dmRoomId = A_ROOM_ID, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_message) + rule.clickOn(CommonStrings.action_message) eventsRecorder.assertSingle(UserProfileEvents.StartDM) } @Test - fun `on Call clicked - the expected callback is called`() = runAndroidComposeUiTest { + fun `on Call clicked - the expected callback is called`() = runTest { ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.AUDIO) { callback -> - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState( dmRoomId = A_ROOM_ID, canCall = true, ), onStartCall = callback, ) - clickOn(CommonStrings.action_call) + rule.clickOn(CommonStrings.action_call) } } @Test - fun `on Video Call clicked - the expected callback is called`() = runAndroidComposeUiTest { + fun `on Video Call clicked - the expected callback is called`() = runTest { ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.VIDEO) { callback -> - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState( dmRoomId = A_ROOM_ID, canCall = true, ), onStartCall = callback, ) - clickOn(CommonStrings.common_video) + rule.clickOn(CommonStrings.common_video) } } @Config(qualifiers = "h1024dp") @Test - fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runAndroidComposeUiTest { + fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runTest { val eventsRecorder = EventsRecorder() - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState( eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_dm_details_block_user) + rule.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`() = runAndroidComposeUiTest { + fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runTest { val eventsRecorder = EventsRecorder() - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_dm_details_block_alert_action) + rule.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`() = runAndroidComposeUiTest { + fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runTest { val eventsRecorder = EventsRecorder() - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } @Config(qualifiers = "h1024dp") @Test - fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runAndroidComposeUiTest { + fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runTest { val eventsRecorder = EventsRecorder() - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_dm_details_unblock_user) + rule.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`() = runAndroidComposeUiTest { + fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runTest { val eventsRecorder = EventsRecorder() - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, eventSink = eventsRecorder, ), ) - clickOn(R.string.screen_dm_details_unblock_alert_action) + rule.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`() = runAndroidComposeUiTest { + fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runTest { val eventsRecorder = EventsRecorder() - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, eventSink = eventsRecorder, ), ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } @Test - fun `on verify user clicked - the right callback is called`() = runAndroidComposeUiTest { + fun `on verify user clicked - the right callback is called`() = runTest { ensureCalledOnceWithParam(A_USER_ID) { callback -> - setUserProfileView( + rule.setUserProfileView( state = aUserProfileState(userId = A_USER_ID, verificationState = UserProfileVerificationState.UNVERIFIED), onVerifyClick = callback, ) - clickOn(CommonStrings.common_verify_user) + rule.clickOn(CommonStrings.common_verify_user) } } } -private fun AndroidComposeUiTest.setUserProfileView( +private fun AndroidComposeTestRule.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 3498ad7714..3219658796 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,12 +6,10 @@ * 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.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState @@ -20,15 +18,18 @@ 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 org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BlockUserDialogsTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `confirm block user emit expected Event`() = runAndroidComposeUiTest { + fun `confirm block user emit expected Event`() { val eventsRecorder = EventsRecorder() - setContent { + rule.setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, @@ -36,14 +37,14 @@ class BlockUserDialogsTest { ) ) } - clickOn(R.string.screen_dm_details_block_alert_action) + rule.clickOn(R.string.screen_dm_details_block_alert_action) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(false)) } @Test - fun `cancel block user emit expected Event`() = runAndroidComposeUiTest { + fun `cancel block user emit expected Event`() { val eventsRecorder = EventsRecorder() - setContent { + rule.setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, @@ -51,14 +52,14 @@ class BlockUserDialogsTest { ) ) } - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } @Test - fun `confirm unblock user emit expected Event`() = runAndroidComposeUiTest { + fun `confirm unblock user emit expected Event`() { val eventsRecorder = EventsRecorder() - setContent { + rule.setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, @@ -66,14 +67,14 @@ class BlockUserDialogsTest { ) ) } - clickOn(R.string.screen_dm_details_unblock_alert_action) + rule.clickOn(R.string.screen_dm_details_unblock_alert_action) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(false)) } @Test - fun `cancel unblock user emit expected Event`() = runAndroidComposeUiTest { + fun `cancel unblock user emit expected Event`() { val eventsRecorder = EventsRecorder() - setContent { + rule.setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, @@ -81,7 +82,7 @@ class BlockUserDialogsTest { ) ) } - clickOn(CommonStrings.action_cancel) + rule.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 895f78cbdc..77f5af4a2e 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,7 +12,6 @@ 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 @@ -60,7 +59,7 @@ internal fun anIncomingSessionVerificationRequest() = VerificationRequest.Incomi details = SessionVerificationRequestDetails( senderProfile = MatrixUser( userId = UserId("@alice:example.com"), - displayName = USER_NAME_ALICE, + displayName = "Alice", avatarUrl = null, ), flowId = FlowId("1234"), @@ -74,7 +73,7 @@ internal fun anIncomingUserVerificationRequest() = VerificationRequest.Incoming. details = SessionVerificationRequestDetails( senderProfile = MatrixUser( userId = UserId("@alice:example.com"), - displayName = USER_NAME_ALICE, + displayName = "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 465fc20066..7932ccc484 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.requestDeviceVerification() + is VerificationRequest.Outgoing.CurrentSession -> sessionVerificationService.requestCurrentSessionVerification() is VerificationRequest.Outgoing.User -> sessionVerificationService.requestUserVerification(event.verificationRequest.userId) } } @@ -56,7 +56,7 @@ class OutgoingVerificationStateMachine( } inState { onEnterEffect { - sessionVerificationService.startSasVerification() + sessionVerificationService.startVerification() } } 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 1c199f8826..2dd2850174 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,10 +24,8 @@ 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 @@ -229,11 +227,7 @@ private fun ContentInitial( Text( modifier = Modifier .clickable { onLearnMoreClick() } - .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 - }, + .padding(vertical = 4.dp, horizontal = 16.dp), 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/VerificationContentVerifying.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationContentVerifying.kt index 891b2a108c..6fc593ffb2 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationContentVerifying.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationContentVerifying.kt @@ -35,7 +35,6 @@ import io.element.android.features.verifysession.impl.emoji.toEmojiResource import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.VerificationEmoji -import io.element.android.libraries.ui.strings.Strings @Composable internal fun VerificationContentVerifying( @@ -50,7 +49,7 @@ internal fun VerificationContentVerifying( ) { when (data) { is SessionVerificationData.Decimals -> { - val text = data.decimals.joinToString(separator = Strings.NICE_SEPARATOR) + val text = data.decimals.joinToString(separator = " - ") Text( modifier = Modifier .fillMaxWidth() 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 c398d72ddd..edaadc583d 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,7 +29,6 @@ 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 @@ -87,7 +86,7 @@ internal fun VerificationUserProfileContentPreview() = ElementPreview( VerificationUserProfileContent( user = MatrixUser( userId = UserId("@alice:example.com"), - displayName = USER_NAME_ALICE, + displayName = "Alice", avatarUrl = "https://example.com/avatar.png", ) ) diff --git a/features/verifysession/impl/src/main/res/values-ca/translations.xml b/features/verifysession/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index d5ca8fb167..0000000000 --- a/features/verifysession/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - "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 2e46202dd4..377da35af3 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -2,8 +2,8 @@ "Bestätigung unmöglich?" "Erstelle einen neuen Wiederherstellungsschlüssel" - "Wähle eine Verifizierungsmethode, um den sicheren Nachrichtenversand einzurichten." - "Bestätige deine digitale Identität" + "Verifiziere dieses Gerät, um sichere Chats einzurichten." + "Bestätige deine 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 beim Senden oder Empfangen von Nachrichten der digitalen Identität dieses Nutzers vertrauen." + "Jetzt kannst du der Identität dieses Nutzers vertrauen, wenn du Nachrichten sendest oder empfängst." "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?" - "Zur zusätzlichen Sicherheit möchte ein anderer Nutzer deine digitale Identität verifizieren. Dir werden einige Emojis zum Abgleich angezeigt." + "Für zusätzliche Sicherheit möchte ein anderer Nutzer deine Identität verifizieren. Es werden dir einige Emojis zum Vergleich 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" @@ -50,5 +50,5 @@ "Nach der Bestätigung kannst du mit der Verifizierung fortfahren." "Akzeptiere die Anfrage für die Verifizierung in deiner anderen Sitzung um fortzufahren." "Warten auf die Annahme der Anfrage" - "Gerät wird entfernt…" + "Abmelden…" 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 25cf6272fe..2c43794b72 100644 --- a/features/verifysession/impl/src/main/res/values-et/translations.xml +++ b/features/verifysession/impl/src/main/res/values-et/translations.xml @@ -2,8 +2,8 @@ "Kas kinnitamine pole võimalik?" "Loo uus taastevõti" - "Turvalise sõnumside seadistamiseks vali verifitseerimise viis." - "Kinnita oma digitaalne identiteet" + "Krüptitud sõnumivahetuse tagamiseks verifitseeri see seade." + "Kinnita, et see oled sina" "Kasuta teist seadet" "Kasuta taastevõtit" "Nüüd saad saata või lugeda sõnumeid turvaliselt ning kõik sinu vestluspartnerid võivad usaldada seda seadet." @@ -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 digitaalset identiteeti usaldada." + "Nüüd sa võid sõnumite vastuvõtmisel ja saatmisel selle kasutaja 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 digitaalse identiteeti verifitseerida. Järgmiseks näed sa emojisid, mida peate omavahel võrdlema." + "Lisaturvalisuse nimel soovib teine kasutaja sinu 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" - "Eemaldan seadet…" + "Logime välja…" 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 3447c07a7d..ffc03e1f19 100644 --- a/features/verifysession/impl/src/main/res/values-fa/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fa/translations.xml @@ -13,7 +13,6 @@ "يه چيزي درست به نظر نمياد یا زمان درخواست به پایان رسید یا درخواست رد شد." "تأیید تطابق شکلک‌های زیر با شکلک‌های نشان داده شده روی افزارهٔ دیگرتان." "مقایسهٔ شکلک‌ها" - "تأیید کنید که اعداد زیر با اعداد نشان داده شده در جلسه دیگر شما مطابقت دارند." "مقایسهٔ اعداد" "اکنون می‌توانید روی افزارهٔ دیگرتان با امنیت پیام فرستاده و بخوانید." "افزاره تأیید شده" @@ -33,5 +32,5 @@ "مطابقند" "برای ادامه، درخواست شروع فرآیند تأیید را در جلسه دیگر خود بپذیرید." "منظر پذیرش درخواست" - "برداشتن افزاره…" + "خارج شدن…" diff --git a/features/verifysession/impl/src/main/res/values-hr/translations.xml b/features/verifysession/impl/src/main/res/values-hr/translations.xml index 91fecd1744..a2ccb7ce0a 100644 --- a/features/verifysession/impl/src/main/res/values-hr/translations.xml +++ b/features/verifysession/impl/src/main/res/values-hr/translations.xml @@ -2,8 +2,8 @@ "Ne možete potvrditi?" "Izradi novi ključ za oporavak" - "Odaberite način potvrde za postavljanje sigurne razmjene poruka." - "Potvrdite svoj digitalni identitet" + "Potvrdite ovaj uređaj kako biste postavili sigurnu razmjenu poruka." + "Potvrdite svoj identitet" "Upotrijebite drugi uređaj" "Upotrijebi ključ za oporavak" "Sada možete sigurno čitati ili slati poruke, a svatko s kim razgovarate također može vjerovati ovom uređaju." @@ -50,5 +50,5 @@ "Nakon prihvaćanja moći ćete nastaviti s potvrđivanjem." "Prihvatite zahtjev za pokretanje postupka provjere u drugoj sesiji kako biste nastavili." "Čekanje na prihvaćanje zahtjeva" - "Uklanjanje uređaja…" + "Odjavljivanje…" 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 05a5bae0ab..021a528646 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 device dari akun…" + "Mengeluarkan 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 39ce9f6fa3..92b0572d08 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" - "Wybierz sposób weryfikacji, aby skonfigurować bezpieczne wiadomości." - "Potwierdź swoją tożsamość cyfrową" + "Zweryfikuj to urządzenie, aby skonfigurować bezpieczne przesyłanie wiadomości." + "Potwierdź, że to Ty" "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" - "Usuwam urządzenie…" + "Wylogowywanie…" 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 2bbda1497a..bf32d3c7b9 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 a tua identidade digital" + "Confirma que és tu" "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 remover dispositivo…" + "A terminar sessão…" 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 25f897d954..0d1ddd0530 100644 --- a/features/verifysession/impl/src/main/res/values-ro/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml @@ -2,8 +2,8 @@ "Nu puteți confirma?" "Creați o nouă cheie de recuperare" - "Alegeți cum doriți să vă verificați pentru a configura mesageria securizată." - "Confirmați-vă identitatea digitală" + "Verificați acest dispozitiv pentru a configura mesagerie securizată." + "Confirmați că sunteți dumneavoastră" "Utilizați un alt dispozitiv" "Utilizați cheia de recuperare" "Acum puteți citi sau trimite mesaje în siguranță, iar oricine cu care conversați poate avea încredere în acest dispozitiv." @@ -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 digitală a acestui utilizator atunci când trimiteți sau primiți mesaje." + "Acum puteți avea încredere în identitatea 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 sporită, un alt utilizator dorește să vă verifice identitatea digitală. Vi se va afișa un set de emoji-uri pentru comparație." + "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." "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-sk/translations.xml b/features/verifysession/impl/src/main/res/values-sk/translations.xml index d4e6f60402..932b8555fc 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" - "Odoberanie zariadenia…" + "Prebieha odhlasovanie…" 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 03fe318d13..1967fef383 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-uz/translations.xml b/features/verifysession/impl/src/main/res/values-uz/translations.xml index 3753b13ec7..26ba7393df 100644 --- a/features/verifysession/impl/src/main/res/values-uz/translations.xml +++ b/features/verifysession/impl/src/main/res/values-uz/translations.xml @@ -2,8 +2,8 @@ "Tasdiqlay olmayapsizmi?" "Yangi tiklash kalitini yarating" - "Xavfsiz xabar almashinuvni sozlash uchun tasdiqlash usulini tanlang." - "Raqamli shaxsingizni tasdiqlang" + "Xavfsiz xabarlashuvni sozlash uchun ushbu qurilmani tasdiqlang." + "Shaxsingizni tasdiqlang" "Boshqa qurilmadan foydalanish" "Qayta tiklash kalitidan foydalaning" "Endi xabarlarni xavfsiz tarzda o‘qish yoki yuborish imkoniyatiga egasiz, shuningdek, siz bilan muloqot qilayotgan har qanday kishi ham bu qurilmaga ishonch bildirishi mumkin." @@ -17,7 +17,7 @@ "Quyidagi raqamlarning boshqa sessiyangizda koʻrsatilgan raqamlarga mos kelishini tasdiqlang." "Sonlarni taqqoslash" "Endi xabarlarni boshqa qurilmangizda xavfsiz o‘qish yoki yuborishingiz mumkin." - "Endi xabarlarni yuborish yoki qabul qilishda bu foydalanuvchining raqamli identifikatoriga ishonishingiz mumkin." + "Endi xabarlarni yuborish yoki qabul qilishda bu foydalanuvchining shaxsiga ishonishingiz mumkin." "Qurilma tasdiqlandi" "Tiklash kalitini kiriting" "So‘rov vaqti tugab qoldi, so‘rov rad etildi yoki tekshiruv mos kelmadi." diff --git a/features/verifysession/impl/src/main/res/values-vi/translations.xml b/features/verifysession/impl/src/main/res/values-vi/translations.xml index 4a4b27ab2a..7cb5b916cf 100644 --- a/features/verifysession/impl/src/main/res/values-vi/translations.xml +++ b/features/verifysession/impl/src/main/res/values-vi/translations.xml @@ -1,14 +1,9 @@ - "Không thể xác nhận?" - "Tạo khóa khôi phục mới" "Chọn phương thức xác minh để bật nhắn tin bảo mật." "Xác nhận danh tính kỹ thuật số của bạn" - "Dùng thiết bị khác" - "Sử dụng khóa khôi phục" "Giờ đây bạn có thể đọc và gửi tin nhắn một cách an toàn, và những người bạn trò chuyện cũng có thể tin tưởng thiết bị này." "Thiết bị được xác thực" - "Dùng thiết bị khác" "Đang chờ trên thiết bị khác…" "Có vẻ như có điều gì đó không đúng. Hoặc yêu cầu đã hết thời gian chờ hoặc yêu cầu đã bị từ chối." "Hãy xác nhận rằng các biểu tượng cảm xúc bên dưới khớp với các biểu tượng hiển thị trên thiết bị khác của bạn." diff --git a/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml b/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml index 5472fba56e..658e5242f6 100644 --- a/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml @@ -2,8 +2,8 @@ "無法確認?" "建立新的復原金鑰" - "選擇驗證方式以設定安全訊息傳遞。" - "確認您的數位身份" + "驗證這部裝置以設定安全通訊。" + "確認這是你本人" "使用另一部裝置" "使用復原金鑰" "您可以安全地讀取和發送訊息了,與您聊天的人也可以信任這部裝置。" @@ -17,7 +17,7 @@ "確認以下數字是否與其他作業階段中顯示的數字相符。" "比較數字" "現在您可以在其他裝置上安全地閱讀或傳送訊息。" - "現在,您可以在傳送或接收訊息時信任此使用者的數位身份。" + "現在,您可以在傳送或接收訊息時信任此使用者的身份。" "裝置已驗證" "輸入復原金鑰" "請求逾時、請求被拒或是驗證不符。" @@ -42,7 +42,7 @@ "在另外一個已驗證的裝置上開啟應用程式" "為了提昇安全性,請透過比較您裝置上的一組表情符號來驗證此使用者。請透過可信的通訊方式來執行此動作。" "驗證此使用者?" - "為了強化安全性,另一位使用者希望驗證您的數位身分。系統將顯示一組表情符號供您比對。" + "為了提昇安全性,另一個使用者希望驗證您的身份。您將會看到一組表情符號以進行比較。" "您應該會在其他裝置上看到一個彈出式視窗。立刻從那裡開始驗證。" "在其他裝置上開始驗證" "在其他裝置上開始驗證" @@ -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 61b7b078dd..b48bfe3f87 100644 --- a/features/verifysession/impl/src/main/res/values-zh/translations.xml +++ b/features/verifysession/impl/src/main/res/values-zh/translations.xml @@ -3,52 +3,52 @@ "无法确认?" "创建新的恢复密钥" "选择验证方式以设置安全的消息传输。" - "确认你的数字身份" - "使用其它设备" + "确认您的数字身份" + "使用其他设备" "使用恢复密钥" - "现在你可以安全地读取或发送消息,并且与你聊天的任何人也可以信任此设备。" + "现在,您可以安全地阅读或发送消息,与您聊天的人也会信任此设备。" "设备已验证" - "使用其它设备" - "正在等待其它设备…" + "使用其他设备" + "正在等待其他设备……" "发生了一些错误。网络请求超时,或者被服务器拒绝。" - "确认以下 Emoji 与你的其它设备上显示的相匹配。" - "比较 Emoji" - "请验证以下 Emoji 是否与对方设备显示的一致" - "确认以下数字与其它会话中显示的一致。" + "确认下面的表情符号与您其他设备上显示的表情符号相匹配。" + "比较表情符号" + "请验证下方表情是否与对方设备显示一致" + "确认以下数字与其他会话中显示的一致。" "比较数字" - "现在可以在其它设备上安全地阅读或发送消息。" - "现在可以在发送或接收消息时信任该用户的数字身份。" + "现在您可以在其他设备上安全地阅读或发送消息。" + "现在您可以在发送或接收消息时信任该用户的数字身份。" "设备已验证" "输入恢复密钥" "要么请求超时,要么请求被拒绝,要么验证不匹配。" - "证明身份以访问加密消息历史。" + "证明自己的身份以访问加密历史消息。" "打开已有会话" "重试验证" "准备就绪" - "正在等待对比…" - "比较一组唯一的 Emoji。" - "比较唯一的 Emoji,确保它们以相同顺序排列。" + "等待比对……" + "比较一组表情符号。" + "比较表情符号,确保它们以相同顺序排列。" "已登录" "要么请求超时,要么请求被拒绝,要么验证不匹配。" "验证失败" "仅在你发起此验证后才继续。" - "验证另一台设备以确保消息历史的安全。" - "现在可以在其它设备上安全地阅读或发送消息。" + "验证另一台设备以确保您的消息历史记录保密。" + "现在您可以在其他设备上安全地阅读或发送消息。" "设备已验证" "已请求验证" "不匹配" "匹配" - "从此处开始验证之前请确保你已在其它设备上打开了 app。" + "从此处开始验证之前,请确保您已在其他设备上打开了该应用程序。" "在另一台验证的设备上打开应用" - "为提高安全性,请通过比较设备上的一组 Emoji 以验证此用户。通过使用安全的方式比如面对面来实施此步骤。" + "为了提高安全性,请通过比较设备上的一组表情符号来验证此用户。通过使用安全方式来做到这一点,如面对面。" "验证此用户?" - "为提高安全性,另一用户想要验证你的数字身份。你将看到一组 Emoij 供你比较。" - "你应该会在另一台设备上看到一个弹出窗口。现在从该处开始验证。" + "为了额外的安全性,另一位用户想要验证您的数字身份。您将看到一组表情符号供您比较。" + "您应该会在另一台设备上看到一个弹出窗口。现在从那里开始验证。" "在另一台设备上开始验证" "在另一台设备上开始验证" - "正在等待其他用户" - "一旦被接受,你将能够继续进行验证。" - "接受此请求以在另一会话中开始验证流程以继续操作。" + "等待其他用户" + "一旦被接受,您将能够继续进行验证。" + "请在其他会话中接受验证请求。" "等待接受请求" - "正在移除设备…" + "正在删除设备……" 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 a8d3802268..c9b9e25b74 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,7 +99,6 @@ 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( @@ -162,7 +161,6 @@ 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( @@ -262,7 +260,6 @@ 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 afab77ad76..4aa63f3ab8 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData @@ -21,55 +18,59 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class IncomingVerificationViewTest { + @get:Rule val rule = createAndroidComposeRule() + // region step Initial @Test - fun `back key pressed - ignore the verification`() = runAndroidComposeUiTest { + fun `back key pressed - ignore the verification`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `ignore incoming verification emits the expected event`() = runAndroidComposeUiTest { + fun `ignore incoming verification emits the expected event`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_ignore) + rule.clickOn(CommonStrings.action_ignore) eventsRecorder.assertSingle(IncomingVerificationViewEvents.IgnoreVerification) } @Test - fun `start incoming verification emits the expected event`() = runAndroidComposeUiTest { + fun `start incoming verification emits the expected event`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_start_verification) + rule.clickOn(CommonStrings.action_start_verification) eventsRecorder.assertSingle(IncomingVerificationViewEvents.StartVerification) } @Test - fun `back key pressed - when awaiting response cancels the verification`() = runAndroidComposeUiTest { + fun `back key pressed - when awaiting response cancels the verification`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial( isWaiting = true, @@ -77,16 +78,16 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } // endregion step Initial // region step Verifying @Test - fun `back key pressed - when ready to verify cancels the verification`() = runAndroidComposeUiTest { + fun `back key pressed - when ready to verify cancels the verification`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -95,14 +96,14 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `back key pressed - when verifying and loading emits the expected event`() = runAndroidComposeUiTest { + fun `back key pressed - when verifying and loading emits the expected event`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -111,14 +112,14 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `clicking on they do not match emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on they do not match emits the expected event`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -127,14 +128,14 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - clickOn(R.string.screen_session_verification_they_dont_match) + rule.clickOn(R.string.screen_session_verification_they_dont_match) eventsRecorder.assertSingle(IncomingVerificationViewEvents.DeclineVerification) } @Test - fun `clicking on they match emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on they match emits the expected event`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -143,35 +144,35 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - clickOn(R.string.screen_session_verification_they_match) + rule.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`() = runAndroidComposeUiTest { + fun `back key pressed - when failure resets the flow`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Failure, eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `click on done - when failure resets the flow`() = runAndroidComposeUiTest { + fun `click on done - when failure resets the flow`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Failure, eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_done) + rule.clickOn(CommonStrings.action_done) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @@ -179,33 +180,33 @@ class IncomingVerificationViewTest { // region step Completed @Test - fun `back key pressed - on Completed step emits the expected event`() = runAndroidComposeUiTest { + fun `back key pressed - on Completed step emits the expected event`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Completed, eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() = runAndroidComposeUiTest { + fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() { val eventsRecorder = EventsRecorder() - setIncomingVerificationView( + rule.setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Completed, eventSink = eventsRecorder ), ) - clickOn(CommonStrings.action_done) + rule.clickOn(CommonStrings.action_done) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } // endregion - private fun AndroidComposeUiTest.setIncomingVerificationView( + private fun AndroidComposeTestRule.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 c6cbfca281..f02f24c9ef 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,8 +6,6 @@ * 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 @@ -29,8 +27,6 @@ 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 @@ -54,11 +50,11 @@ class OutgoingVerificationPresenterTest { @Test fun `present - Handles requestVerification for session verification`() = runTest { - val requestDeviceVerificationRecorder = lambdaRecorder {} - val startSasVerificationRecorder = lambdaRecorder {} + val requestSessionVerificationRecorder = lambdaRecorder {} + val startVerificationRecorder = lambdaRecorder {} val service = unverifiedSessionService( - requestDeviceVerificationLambda = requestDeviceVerificationRecorder, - startSasVerificationLambda = startSasVerificationRecorder, + requestSessionVerificationLambda = requestSessionVerificationRecorder, + startVerificationLambda = startVerificationRecorder, ) val presenter = createOutgoingVerificationPresenter( service = service, @@ -67,18 +63,18 @@ class OutgoingVerificationPresenterTest { presenter.test { requestVerificationAndAwaitVerifyingState(service) - requestDeviceVerificationRecorder.assertions().isCalledOnce() - startSasVerificationRecorder.assertions().isCalledOnce() + requestSessionVerificationRecorder.assertions().isCalledOnce() + startVerificationRecorder.assertions().isCalledOnce() } } @Test fun `present - Handles requestVerification for user verification`() = runTest { val requestUserVerificationRecorder = lambdaRecorder {} - val startSasVerificationRecorder = lambdaRecorder {} + val startVerificationRecorder = lambdaRecorder {} val service = unverifiedSessionService( requestUserVerificationLambda = requestUserVerificationRecorder, - startSasVerificationLambda = startSasVerificationRecorder, + startVerificationLambda = startVerificationRecorder, ) val presenter = createOutgoingVerificationPresenter( service = service, @@ -88,7 +84,7 @@ class OutgoingVerificationPresenterTest { requestVerificationAndAwaitVerifyingState(service) requestUserVerificationRecorder.assertions().isCalledOnce() - startSasVerificationRecorder.assertions().isCalledOnce() + startVerificationRecorder.assertions().isCalledOnce() } } @@ -110,8 +106,8 @@ class OutgoingVerificationPresenterTest { @Test fun `present - A failure when verifying cancels it`() = runTest { val service = unverifiedSessionService( - requestDeviceVerificationLambda = { }, - startSasVerificationLambda = { }, + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, approveVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) @@ -129,7 +125,7 @@ class OutgoingVerificationPresenterTest { @Test fun `present - A fail when requesting verification resets the state to the canceled one`() = runTest { val service = unverifiedSessionService( - requestDeviceVerificationLambda = { }, + requestSessionVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) presenter.test { @@ -143,8 +139,8 @@ class OutgoingVerificationPresenterTest { @Test fun `present - Canceling the flow once it's verifying cancels it`() = runTest { val service = unverifiedSessionService( - requestDeviceVerificationLambda = { }, - startSasVerificationLambda = { }, + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, cancelVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) @@ -158,8 +154,8 @@ class OutgoingVerificationPresenterTest { @Test fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { val service = unverifiedSessionService( - requestDeviceVerificationLambda = { }, - startSasVerificationLambda = { }, + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) presenter.test { @@ -172,8 +168,8 @@ class OutgoingVerificationPresenterTest { @Test fun `present - Go back after cancellation returns to initial state`() = runTest { val service = unverifiedSessionService( - requestDeviceVerificationLambda = { }, - startSasVerificationLambda = { }, + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) presenter.test { @@ -193,8 +189,8 @@ class OutgoingVerificationPresenterTest { VerificationEmoji(number = 30) ) val service = unverifiedSessionService( - requestDeviceVerificationLambda = { }, - startSasVerificationLambda = { }, + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, approveVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) @@ -219,8 +215,8 @@ class OutgoingVerificationPresenterTest { @Test fun `present - When verification is declined, the flow is canceled`() = runTest { val service = unverifiedSessionService( - requestDeviceVerificationLambda = { }, - startSasVerificationLambda = { }, + requestSessionVerificationLambda = { }, + startVerificationLambda = { }, declineVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) @@ -275,7 +271,6 @@ class OutgoingVerificationPresenterTest { } } - context(testScope: TestScope) private suspend fun ReceiveTurbine.requestVerificationAndAwaitVerifyingState( fakeService: FakeSessionVerificationService, sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), @@ -283,7 +278,6 @@ 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() @@ -292,7 +286,6 @@ 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() @@ -304,29 +297,30 @@ class OutgoingVerificationPresenterTest { return state } - private fun unverifiedSessionService( - requestDeviceVerificationLambda: () -> Unit = { lambdaError() }, + private suspend fun unverifiedSessionService( + requestSessionVerificationLambda: () -> Unit = { lambdaError() }, requestUserVerificationLambda: (UserId) -> Unit = { lambdaError() }, cancelVerificationLambda: () -> Unit = { lambdaError() }, approveVerificationLambda: () -> Unit = { lambdaError() }, declineVerificationLambda: () -> Unit = { lambdaError() }, - startSasVerificationLambda: () -> Unit = { lambdaError() }, + startVerificationLambda: () -> Unit = { lambdaError() }, resetLambda: (Boolean) -> Unit = { }, acknowledgeVerificationRequestLambda: (VerificationRequest.Incoming) -> Unit = { lambdaError() }, acceptVerificationRequestLambda: () -> Unit = { lambdaError() }, ): FakeSessionVerificationService { return FakeSessionVerificationService( - initialSessionVerifiedStatus = SessionVerifiedStatus.NotVerified, - requestDeviceVerificationLambda = requestDeviceVerificationLambda, + requestCurrentSessionVerificationLambda = requestSessionVerificationLambda, requestUserVerificationLambda = requestUserVerificationLambda, cancelVerificationLambda = cancelVerificationLambda, approveVerificationLambda = approveVerificationLambda, declineVerificationLambda = declineVerificationLambda, - startSasVerificationLambda = startSasVerificationLambda, + startVerificationLambda = startVerificationLambda, 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 1c96c5c2af..71b55fac10 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,14 +6,11 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData @@ -24,54 +21,58 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class OutgoingVerificationViewTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `back key pressed - when canceled resets the flow`() = runAndroidComposeUiTest { + fun `back key pressed - when canceled resets the flow`() { val eventsRecorder = EventsRecorder() - setOutgoingVerificationView( + rule.setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Canceled, eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Reset) } @Test - fun `back key pressed - when awaiting response cancels the verification`() = runAndroidComposeUiTest { + fun `back key pressed - when awaiting response cancels the verification`() { val eventsRecorder = EventsRecorder() - setOutgoingVerificationView( + rule.setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.AwaitingOtherDeviceResponse, eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) } @Test - fun `back key pressed - when ready to verify cancels the verification`() = runAndroidComposeUiTest { + fun `back key pressed - when ready to verify cancels the verification`() { val eventsRecorder = EventsRecorder() - setOutgoingVerificationView( + rule.setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Ready, eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) } @Test - fun `back key pressed - when verifying and not loading declines the verification`() = runAndroidComposeUiTest { + fun `back key pressed - when verifying and not loading declines the verification`() { val eventsRecorder = EventsRecorder() - setOutgoingVerificationView( + rule.setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -80,14 +81,14 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) } @Test - fun `back key pressed - when verifying and loading does nothing`() = runAndroidComposeUiTest { + fun `back key pressed - when verifying and loading does nothing`() { val eventsRecorder = EventsRecorder() - setOutgoingVerificationView( + rule.setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -96,42 +97,42 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - pressBackKey() + rule.pressBackKey() eventsRecorder.assertEmpty() } @Test - fun `back key pressed - on Completed exits the flow`() = runAndroidComposeUiTest { + fun `back key pressed - on Completed exits the flow`() { ensureCalledOnce { callback -> - setOutgoingVerificationView( + rule.setOutgoingVerificationView( onBack = callback, state = anOutgoingVerificationState( step = OutgoingVerificationState.Step.Completed, ), ) - pressBackKey() + rule.pressBackKey() } } @Test - fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() = runAndroidComposeUiTest { + fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - setOutgoingVerificationView( + rule.setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Completed, eventSink = eventsRecorder ), onFinished = callback, ) - clickOn(CommonStrings.action_continue) + rule.clickOn(CommonStrings.action_continue) } } @Test - fun `clicking on they match emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on they match emits the expected event`() { val eventsRecorder = EventsRecorder() - setOutgoingVerificationView( + rule.setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -140,14 +141,14 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - clickOn(R.string.screen_session_verification_they_match) + rule.clickOn(R.string.screen_session_verification_they_match) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.ConfirmVerification) } @Test - fun `clicking on they do not match emits the expected event`() = runAndroidComposeUiTest { + fun `clicking on they do not match emits the expected event`() { val eventsRecorder = EventsRecorder() - setOutgoingVerificationView( + rule.setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -156,11 +157,11 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - clickOn(R.string.screen_session_verification_they_dont_match) + rule.clickOn(R.string.screen_session_verification_they_dont_match) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) } - private fun AndroidComposeUiTest.setOutgoingVerificationView( + private fun AndroidComposeTestRule.setOutgoingVerificationView( state: OutgoingVerificationState, onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(), diff --git a/features/wallet/api/build.gradle.kts b/features/wallet/api/build.gradle.kts new file mode 100644 index 0000000000..351d8373c2 --- /dev/null +++ b/features/wallet/api/build.gradle.kts @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.wallet.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) +} diff --git a/features/wallet/api/src/main/AndroidManifest.xml b/features/wallet/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0baf68a8a8 --- /dev/null +++ b/features/wallet/api/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/CardanoClient.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/CardanoClient.kt new file mode 100644 index 0000000000..5326ac09e5 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/CardanoClient.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * Client interface for interacting with the Cardano blockchain. + * + * All methods are suspend functions and return [Result] to handle errors gracefully. + * Implementations should handle retries, rate limiting, and network errors internally. + */ +interface CardanoClient { + /** + * Get the balance (in lovelace) for a given Cardano address. + * + * @param address Bech32 Cardano address (addr1... or addr_test1...) + * @return Balance in lovelace (1 ADA = 1,000,000 lovelace) + */ + suspend fun getBalance(address: String): Result + + /** + * Get all unspent transaction outputs (UTxOs) for a given address. + * + * @param address Bech32 Cardano address + * @return List of [Utxo] objects representing available outputs + */ + suspend fun getUtxos(address: String): Result> + + /** + * Submit a signed transaction to the Cardano network. + * + * @param signedTxCbor CBOR-encoded signed transaction as hex string + * @return Transaction hash on success + */ + suspend fun submitTx(signedTxCbor: String): Result + + /** + * Get the current status of a transaction. + * + * @param txHash Transaction hash to query + * @return Current [TxStatus] of the transaction + */ + suspend fun getTxStatus(txHash: String): Result + + /** + * Get the current protocol parameters from the network. + * + * Protocol parameters are needed for fee calculation and transaction building. + * + * @return Current [ProtocolParameters] from the latest epoch + */ + suspend fun getProtocolParameters(): Result + + /** + * Get native assets (tokens) for a given address. + * + * @param address Bech32 Cardano address + * @return List of [NativeAsset] objects + */ + suspend fun getAddressAssets(address: String): Result> + + /** + * Get transaction history for a given address. + * + * @param address Bech32 Cardano address + * @param limit Maximum number of transactions to return (default 20) + * @return List of [TxSummary] objects, most recent first + */ + suspend fun getAddressTransactions(address: String, limit: Int = 20): Result> + + /** + * Resolve an ADA Handle to a Cardano address. + * + * ADA Handles are human-readable names (e.g., $cobb) that resolve to Cardano addresses. + * Handle resolution is case-insensitive. + * + * @param handle Handle name WITHOUT the $ prefix (e.g., "cobb" not "$cobb") + * @return Bech32 Cardano address if handle exists, null if not found + */ + suspend fun resolveHandle(handle: String): Result + + /** + * Get CIP-25 NFT metadata for a specific asset. + * + * Uses the Koios asset_info endpoint to fetch onchain_metadata. + * + * @param policyId The minting policy ID (hex, 56 chars) + * @param assetName The asset name (hex encoded) + * @return [NftMetadata] if CIP-25 metadata exists, null otherwise + */ + suspend fun getNftMetadata(policyId: String, assetName: String): Result +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/CardanoException.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/CardanoException.kt new file mode 100644 index 0000000000..12f8797d5a --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/CardanoException.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * Base exception for Cardano-related errors. + */ +sealed class CardanoException( + override val message: String, + override val cause: Throwable? = null, +) : Exception(message, cause) { + + /** + * Network connectivity or API error. + */ + class NetworkException( + message: String, + val statusCode: Int? = null, + cause: Throwable? = null, + ) : CardanoException(message, cause) + + /** + * Rate limit exceeded (HTTP 429). + */ + class RateLimitException( + message: String = "Rate limit exceeded", + val retryAfterMs: Long? = null, + ) : CardanoException(message) + + /** + * Invalid Cardano address format. + */ + class InvalidAddressException( + val address: String, + ) : CardanoException("Invalid Cardano address: $address") + + /** + * Transaction not found on chain. + */ + class TransactionNotFoundException( + val txHash: String, + ) : CardanoException("Transaction not found: $txHash") + + /** + * Transaction submission failed. + */ + class SubmissionFailedException( + message: String, + val errorCode: String? = null, + cause: Throwable? = null, + ) : CardanoException(message, cause) + + /** + * Insufficient funds to complete transaction. + */ + class InsufficientFundsException( + val required: Long, + val available: Long, + ) : CardanoException("Insufficient funds: required $required lovelace, available $available lovelace") + + /** + * Generic API error for unexpected responses. + */ + class ApiException( + message: String, + val response: String? = null, + cause: Throwable? = null, + ) : CardanoException(message, cause) +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/NativeAsset.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/NativeAsset.kt new file mode 100644 index 0000000000..573da19b20 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/NativeAsset.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * Represents a native asset (token) on Cardano. + * + * @property policyId The minting policy ID (hex) + * @property assetName The asset name (hex or decoded) + * @property quantity The amount of this asset + * @property displayName Human-readable name if available + * @property fingerprint The asset fingerprint (CIP-14) + * @property imageUrl Resolved image URL (IPFS gateway or HTTPS) for NFTs + * @property decimals Decimal places for fungible tokens (null for NFTs) + * @property ticker Token ticker symbol (e.g., "HOSKY") + * @property description Token/NFT description + * @property isNft True if this is likely an NFT (quantity == 1 with image metadata) + */ +data class NativeAsset( + val policyId: String, + val assetName: String, + val quantity: Long, + val displayName: String?, + val fingerprint: String?, + val imageUrl: String? = null, + val decimals: Int? = null, + val ticker: String? = null, + val description: String? = null, + val isNft: Boolean = false, +) { + /** + * Truncated policy ID for display. + */ + val truncatedPolicyId: String + get() = if (policyId.length > 16) { + "${policyId.take(8)}...${policyId.takeLast(8)}" + } else { + policyId + } + + /** + * Display name, falling back to truncated asset name. + */ + val name: String + get() = displayName ?: ticker ?: assetName.takeIf { it.isNotEmpty() }?.let { + // Try to decode hex to ASCII if it looks printable + try { + val decoded = it.chunked(2).map { hex -> hex.toInt(16).toChar() }.joinToString("") + if (decoded.all { c -> c.isLetterOrDigit() || c in " -_" }) decoded else it + } catch (_: Exception) { + it + } + } ?: "Unknown" + + /** + * Unit string for this asset (concatenated policyId + assetName). + */ + val unit: String + get() = "$policyId$assetName" + + /** + * Format quantity with decimals for display. + */ + fun formatQuantity(): String { + return if (decimals != null && decimals > 0) { + val divisor = Math.pow(10.0, decimals.toDouble()) + String.format("%.${decimals}f", quantity / divisor).trimEnd('0').trimEnd('.') + } else { + quantity.toString() + } + } +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/NftMetadata.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/NftMetadata.kt new file mode 100644 index 0000000000..3cc6c1dee3 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/NftMetadata.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * CIP-25 NFT metadata parsed from Koios asset_info response. + * + * @property name The NFT name + * @property image Resolved HTTP URL for the image (IPFS gateway or direct HTTPS) + * @property description NFT description if available + * @property rawMetadata Original metadata map for additional fields + */ +data class NftMetadata( + val name: String, + val image: String?, + val description: String?, + val rawMetadata: Map, +) { + companion object { + private const val IPFS_GATEWAY = "https://ipfs.io/ipfs/" + + /** + * Resolve IPFS URLs to HTTP gateway URLs. + */ + fun resolveImageUrl(url: String?): String? { + if (url == null) return null + return when { + url.startsWith("ipfs://") -> IPFS_GATEWAY + url.removePrefix("ipfs://") + url.startsWith("Qm") -> IPFS_GATEWAY + url // Direct IPFS hash + url.startsWith("https://") || url.startsWith("http://") -> url + else -> null + } + } + + /** + * Join array-based image URL (some NFTs split the URL across multiple strings). + */ + fun joinImageParts(parts: List?): String? { + if (parts.isNullOrEmpty()) return null + return resolveImageUrl(parts.joinToString("")) + } + } +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentCardStatus.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentCardStatus.kt new file mode 100644 index 0000000000..acd5d0a6ed --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentCardStatus.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * Status of a Cardano payment transaction. + */ +enum class PaymentCardStatus { + /** Transaction submitted but not yet confirmed on chain */ + PENDING, + /** Transaction confirmed on chain */ + CONFIRMED, + /** Transaction failed or was rejected */ + FAILED +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentEventSender.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentEventSender.kt new file mode 100644 index 0000000000..8ac0eba8d6 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentEventSender.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +import io.element.android.libraries.matrix.api.timeline.Timeline + +/** + * Interface for sending Cardano payment events to Matrix rooms. + */ +interface PaymentEventSender { + /** + * Send a payment event to the room timeline. + * + * This creates a specially formatted message that wallet-enabled clients + * can render as a payment card, while non-wallet clients see a fallback text. + * + * @param timeline The room timeline to send the event to + * @param request The payment request details + * @param signedTx The signed transaction (contains tx hash) + * @param network The Cardano network (mainnet/testnet) + * @return Result indicating success or failure + */ + suspend fun sendPaymentEvent( + timeline: Timeline, + request: PaymentRequest, + signedTx: SignedTransaction, + network: String, + ): Result + + /** + * Send a payment status update event. + * + * Used when a transaction's status changes (e.g., pending → confirmed). + * + * @param timeline The room timeline + * @param txHash The transaction hash + * @param newStatus The new status + * @param network The Cardano network + * @return Result indicating success or failure + */ + suspend fun sendStatusUpdate( + timeline: Timeline, + txHash: String, + newStatus: String, + network: String, + ): Result +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentRequest.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentRequest.kt new file mode 100644 index 0000000000..55ac2ecc12 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentRequest.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +import io.element.android.libraries.matrix.api.core.SessionId + +/** + * A request to build and sign a Cardano payment transaction. + * + * @property fromAddress The sender's Cardano address (Bech32) + * @property toAddress The recipient's Cardano address (Bech32) + * @property amountLovelace The amount of ADA to send in lovelace (1 ADA = 1,000,000 lovelace). + * For token-only sends, this should be the minimum UTXO (~1.5 ADA). + * @property sessionId The Matrix session ID for key retrieval + * @property assetPolicyId Policy ID of the native asset to send (null for ADA-only) + * @property assetName Asset name in hex (null for ADA-only) + * @property assetQuantity Quantity of the native asset to send (null for ADA-only) + */ +data class PaymentRequest( + val fromAddress: String, + val toAddress: String, + val amountLovelace: Long, + val sessionId: SessionId, + val assetPolicyId: String? = null, + val assetName: String? = null, + val assetQuantity: Long? = null, +) { + /** + * True if this request includes a native asset (token) send. + */ + val hasAsset: Boolean + get() = assetPolicyId != null && assetName != null && assetQuantity != null && assetQuantity > 0 +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentStatusPoller.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentStatusPoller.kt new file mode 100644 index 0000000000..b404a8fbae --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/PaymentStatusPoller.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +import kotlinx.coroutines.flow.Flow + +/** + * Interface for polling transaction confirmation status. + */ +interface PaymentStatusPoller { + /** + * Polls for transaction confirmation status. + * + * Emits [TxStatus] changes as a Flow: + * - Initially PENDING + * - CONFIRMED when transaction is in a block + * - FAILED if confirmation times out or error occurs + * + * Polling behavior: + * - Poll every 10 seconds + * - Maximum 60 attempts (~10 minutes total) + * - Stops when status changes from PENDING + * + * @param txHash The transaction hash to poll + * @return Flow of [TxStatus] changes + */ + fun pollUntilConfirmed(txHash: String): Flow +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/ProtocolParameters.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/ProtocolParameters.kt new file mode 100644 index 0000000000..e9faddc71d --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/ProtocolParameters.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * Cardano protocol parameters needed for transaction building and fee calculation. + * + * These parameters are set via governance and determine transaction costs + * and constraints on the network. + * + * @property minFeeA The linear fee coefficient (lovelace per byte) + * @property minFeeB The constant fee (base fee in lovelace) + * @property maxTxSize Maximum transaction size in bytes + * @property utxoCostPerByte Cost in lovelace per byte of UTXO storage (for min UTXO calculation) + */ +data class ProtocolParameters( + val minFeeA: Long, + val minFeeB: Long, + val maxTxSize: Int, + val utxoCostPerByte: Long, +) diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/SignedTransaction.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/SignedTransaction.kt new file mode 100644 index 0000000000..43163ce285 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/SignedTransaction.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * A signed Cardano transaction ready for submission. + * + * @property txCbor The CBOR-encoded signed transaction as a hex string + * @property txHash The transaction hash (for tracking) + * @property fee The transaction fee in lovelace + * @property actualAmount The actual amount sent (may differ slightly from requested due to min UTXO rules) + */ +data class SignedTransaction( + val txCbor: String, + val txHash: String, + val fee: Long, + val actualAmount: Long, +) diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/TransactionBuilder.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/TransactionBuilder.kt new file mode 100644 index 0000000000..aba618bec3 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/TransactionBuilder.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * Interface for building and signing Cardano transactions. + * + * The implementation handles: + * - UTXO selection (largest-first coin selection) + * - Fee calculation based on protocol parameters + * - Change output calculation + * - Transaction signing with the spending key + * + * ## Error handling + * The following errors may be returned: + * - [CardanoException.InsufficientFundsException] - Not enough ADA in wallet + * - [CardanoException.InvalidAddressException] - Invalid address format + * - [CardanoException.ApiException] - Various API/build errors + */ +interface TransactionBuilder { + /** + * Builds and signs a payment transaction. + * + * This method will: + * 1. Fetch UTXOs for the sender address + * 2. Select UTXOs to cover amount + fee (largest-first) + * 3. Build the transaction with proper change output + * 4. Retrieve the spending key (triggers biometric prompt) + * 5. Sign the transaction + * 6. Return the signed transaction ready for submission + * + * @param request The payment request details + * @return [SignedTransaction] on success, error on failure + */ + suspend fun buildAndSign(request: PaymentRequest): Result +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/TxStatus.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/TxStatus.kt new file mode 100644 index 0000000000..cfc63b9aa3 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/TxStatus.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * Transaction confirmation status on the Cardano blockchain. + */ +enum class TxStatus { + /** Transaction submitted but not yet confirmed in a block. */ + PENDING, + + /** Transaction confirmed in at least one block. */ + CONFIRMED, + + /** Transaction failed or was rejected by the network. */ + FAILED, +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/TxSummary.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/TxSummary.kt new file mode 100644 index 0000000000..8fb5c01026 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/TxSummary.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +/** + * Summary of a Cardano transaction for history display. + * + * @property txHash The transaction hash + * @property blockTime Unix timestamp when the tx was included in a block + * @property totalOutput Total output in lovelace + * @property fee Transaction fee in lovelace + * @property direction Whether this was sent or received + */ +data class TxSummary( + val txHash: String, + val blockTime: Long, + val totalOutput: Long, + val fee: Long, + val direction: Direction, +) { + enum class Direction { + SENT, + RECEIVED, + } + + /** + * Formatted date for display. + */ + val formattedDate: String + get() = try { + val instant = Instant.ofEpochSecond(blockTime) + val formatter = DateTimeFormatter.ofPattern("MMM d, yyyy") + .withZone(ZoneId.systemDefault()) + formatter.format(instant) + } catch (_: Exception) { + "Unknown date" + } + + /** + * Truncated tx hash for display. + */ + val truncatedTxHash: String + get() = if (txHash.length > 16) { + "${txHash.take(8)}...${txHash.takeLast(8)}" + } else { + txHash + } + + /** + * Amount formatted as ADA. + */ + val amountAda: String + get() { + val ada = totalOutput / 1_000_000.0 + return if (ada == ada.toLong().toDouble()) { + "${ada.toLong()} ADA" + } else { + val formatted = "%.6f".format(ada).trimEnd('0').trimEnd('.') + "$formatted ADA" + } + } + + /** + * Explorer URL for this transaction. + */ + fun explorerUrl(isTestnet: Boolean): String { + return if (isTestnet) { + "https://preprod.cardanoscan.io/transaction/$txHash" + } else { + "https://cardanoscan.io/transaction/$txHash" + } + } +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/Utxo.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/Utxo.kt new file mode 100644 index 0000000000..b54d1b4759 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/Utxo.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * Represents an unspent transaction output (UTxO) on Cardano. + * + * @property txHash The transaction hash where this UTxO was created. + * @property outputIndex The index of this output within the transaction. + * @property amount The amount in lovelace (1 ADA = 1,000,000 lovelace). + * @property address The address holding this UTxO. + * @property assets Native assets (tokens) contained in this UTxO. + */ +data class Utxo( + val txHash: String, + val outputIndex: Int, + val amount: Long, + val address: String, + val assets: List = emptyList(), +) + +/** + * Represents a native asset within a UTxO. + * + * @property policyId The minting policy ID (56 hex chars). + * @property assetName The asset name (hex-encoded). + * @property quantity The amount of this asset in the UTxO. + */ +data class UtxoAsset( + val policyId: String, + val assetName: String, + val quantity: Long, +) diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/WalletEntryPoint.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/WalletEntryPoint.kt new file mode 100644 index 0000000000..9d08208c80 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/WalletEntryPoint.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId + +/** + * Entry point for the Cardano wallet feature. + * Provides navigation to payment flows and wallet management. + */ +interface WalletEntryPoint : FeatureEntryPoint { + /** + * Builder for creating wallet flow nodes. + */ + interface Builder { + fun setRoomId(roomId: RoomId): Builder + fun setRecipientUserId(userId: UserId?): Builder + fun setRecipientAddress(address: String?): Builder + fun setAmount(amount: String?): Builder + fun build(): Node + } + + /** + * Creates a builder for the payment flow. + */ + fun paymentFlowBuilder( + parentNode: Node, + buildContext: BuildContext, + callback: Callback, + ): Builder + + /** + * Callback for wallet flow events. + */ + interface Callback : Plugin { + fun onPaymentSent(txHash: String) + fun onPaymentCancelled() + /** Called when user needs to set up wallet before paying. Caller should navigate to wallet panel. */ + fun onOpenWalletSettings() + } +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/WalletState.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/WalletState.kt new file mode 100644 index 0000000000..42e4f134a9 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/WalletState.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api + +/** + * Represents the current state of the Cardano wallet. + */ +data class WalletState( + val hasWallet: Boolean, + val address: String?, + val balanceLovelace: Long?, + val balanceAda: String?, + val isLoading: Boolean, + val error: String?, +) { + companion object { + val Initial = WalletState( + hasWallet = false, + address = null, + balanceLovelace = null, + balanceAda = null, + isLoading = true, + error = null, + ) + } +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/address/CardanoAddressService.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/address/CardanoAddressService.kt new file mode 100644 index 0000000000..2464440fa5 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/address/CardanoAddressService.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api.address + +import io.element.android.libraries.matrix.api.core.UserId + +/** + * Service for managing Cardano addresses in Matrix account data. + * + * This allows users to publish their Cardano address so other users can + * look it up for payments - like a public address directory baked into Matrix. + * + * Account data key: `com.sulkta.cardano.address` + * Content format: `{ "address": "addr1..." }` + */ +interface CardanoAddressService { + /** + * Publish the user's Cardano address to their Matrix account data. + * This is public data, not encrypted. + * + * @param address The Cardano address to publish + * @return Result indicating success or failure + */ + suspend fun publishAddress(address: String): Result + + /** + * Look up another user's Cardano address from their Matrix account data. + * + * @param userId The Matrix user ID to look up + * @return The user's Cardano address if published, null if not found + */ + suspend fun lookupAddress(userId: UserId): Result + + companion object { + const val ACCOUNT_DATA_TYPE = "com.sulkta.cardano.address" + } +} + +/** + * Result of a Cardano address lookup. + */ +sealed interface AddressLookupResult { + /** Address was found and retrieved successfully */ + data class Found(val address: String, val userId: UserId) : AddressLookupResult + + /** User has no Cardano address linked */ + data class NotLinked(val userId: UserId) : AddressLookupResult + + /** Lookup failed due to an error */ + data class Error(val userId: UserId, val message: String) : AddressLookupResult +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/backup/WalletBackupService.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/backup/WalletBackupService.kt new file mode 100644 index 0000000000..6d5aba7a34 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/backup/WalletBackupService.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api.backup + +/** + * Service for backing up and restoring wallet seed phrases using Matrix SSSS. + * + * The backup is encrypted with the user's Matrix recovery key and stored + * in their account data, so it follows them across devices. + */ +interface WalletBackupService { + /** + * The secret name used to store the wallet seed in SSSS. + */ + companion object { + const val SECRET_NAME = "com.sulkta.cardano.wallet_seed" + } + + /** + * Backup the wallet seed phrase to Matrix SSSS. + * + * @param recoveryKey The Matrix recovery key (base58 encoded) + * @param mnemonic The wallet seed phrase to backup + * @return Success or error + */ + suspend fun backupSeed(recoveryKey: String, mnemonic: List): Result + + /** + * Restore a wallet seed phrase from Matrix SSSS. + * + * @param recoveryKey The Matrix recovery key + * @return The mnemonic words if found, null if no backup exists + */ + suspend fun restoreSeed(recoveryKey: String): Result?> + + /** + * Check if a wallet backup exists in SSSS. + * + * This can be called with the recovery key to verify a backup is present. + * + * @param recoveryKey The Matrix recovery key + * @return True if a backup exists, false otherwise + */ + suspend fun hasBackup(recoveryKey: String): Result + + /** + * Check if a wallet backup exists in account data WITHOUT decrypting. + * + * This checks the raw Matrix account data to see if the secret key exists, + * without needing the recovery key. Useful for UI to show restore option. + * + * @return True if the account data key exists, false otherwise + */ + suspend fun hasBackupWithoutKey(): Result +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/storage/CardanoKeyStorage.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/storage/CardanoKeyStorage.kt new file mode 100644 index 0000000000..a36e64ebfd --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/storage/CardanoKeyStorage.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api.storage + +import io.element.android.libraries.matrix.api.core.SessionId + +/** + * Result of wallet creation containing the generated seed phrase and derived addresses. + */ +data class WalletCreationResult( + val mnemonic: List, + val baseAddress: String, + val stakeAddress: String, +) + +/** + * Interface for secure storage and retrieval of Cardano wallet keys. + * + * Wallets are scoped PER SESSION (per Matrix account). Each [SessionId] can have + * exactly one wallet associated with it. + * + * ## Security Properties + * - Keys are stored encrypted using Android Keystore + * - Biometric/PIN authentication required for every signing operation + * - Keys are INVALIDATED if biometric enrollment changes + * - Mnemonic is stored encrypted, never in plaintext + * + * ## Implementation Notes + * - Use `setInvalidatedByBiometricEnrollment(true)` for Keystore keys + * - Use `setUserAuthenticationRequired(true)` with duration -1 (every time) + * - Key alias format: "cardano_wallet_{sessionId}" + */ +interface CardanoKeyStorage { + /** + * Checks if a wallet exists for the given session. + */ + suspend fun hasWallet(sessionId: SessionId): Boolean + + /** + * Generates a new wallet with a 24-word BIP-39 mnemonic. + * + * @param sessionId The Matrix session to create the wallet for + * @return [WalletCreationResult] containing the mnemonic and derived addresses + * @throws IllegalStateException if a wallet already exists for this session + */ + suspend fun generateWallet(sessionId: SessionId): Result + + /** + * Imports an existing wallet from a mnemonic phrase. + * + * @param sessionId The Matrix session to import the wallet for + * @param mnemonic The BIP-39 mnemonic phrase (12, 15, 18, 21, or 24 words) + * @return The derived base address on success + * @throws IllegalArgumentException if the mnemonic is invalid + * @throws IllegalStateException if a wallet already exists for this session + */ + suspend fun importWallet(sessionId: SessionId, mnemonic: List): Result + + /** + * Retrieves the encrypted mnemonic for backup display. + * + * ⚠️ WARNING: This returns sensitive data. UI must use FLAG_SECURE. + * + * @param sessionId The Matrix session + * @return The mnemonic word list + */ + suspend fun getMnemonic(sessionId: SessionId): Result> + + /** + * Gets the base address (payment + staking key hash) for the wallet. + * + * @param sessionId The Matrix session + * @param addressIndex The address index (default 0) + */ + suspend fun getBaseAddress(sessionId: SessionId, addressIndex: Int = 0): Result + + /** + * Gets the staking/reward address for the wallet. + * + * @param sessionId The Matrix session + */ + suspend fun getStakeAddress(sessionId: SessionId): Result + + /** + * Permanently deletes the wallet and all associated key material. + * + * @param sessionId The Matrix session + */ + suspend fun deleteWallet(sessionId: SessionId): Result +} diff --git a/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/timeline/TimelineItemPaymentContent.kt b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/timeline/TimelineItemPaymentContent.kt new file mode 100644 index 0000000000..6a3eeebc29 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/io/element/android/features/wallet/api/timeline/TimelineItemPaymentContent.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.api.timeline + +import androidx.compose.runtime.Immutable +import io.element.android.features.wallet.api.PaymentCardStatus + +/** + * Timeline content for a Cardano payment event. + * + * This class represents payment event content and can be rendered + * in the timeline. It does NOT inherit from TimelineItemEventContent + * to avoid circular dependencies between wallet:api and messages:impl. + * + * The TimelineItemContentFactory handles this type specially. + * + * @property amountLovelace The payment amount in lovelace (1 ADA = 1,000,000 lovelace) + * @property toAddress The recipient Cardano address (Bech32) + * @property fromAddress The sender Cardano address (Bech32) + * @property txHash The transaction hash (null if not yet submitted) + * @property status Current status of the payment + * @property network The Cardano network (mainnet/testnet) + * @property isSentByMe True if the current user sent this payment + * @property fallbackText Human-readable fallback text for non-wallet clients + */ +@Immutable +data class TimelineItemPaymentContent( + val amountLovelace: Long, + val toAddress: String, + val fromAddress: String, + val txHash: String?, + val status: PaymentCardStatus, + val network: String, + val isSentByMe: Boolean, + val fallbackText: String, +) { + val type: String = EVENT_TYPE + + /** + * Amount formatted in ADA (lovelace / 1,000,000). + */ + val amountAda: String + get() = formatAda(amountLovelace) + + /** + * Whether this is on testnet. + */ + val isTestnet: Boolean + get() = network == "testnet" || network == "preprod" || network == "preview" + + /** + * Truncated tx hash for display (first 8 + last 8 chars). + */ + val truncatedTxHash: String? + get() = txHash?.let { hash -> + if (hash.length > 20) { + "${hash.take(8)}...${hash.takeLast(8)}" + } else { + hash + } + } + + /** + * Truncated recipient address for display (first 8 + last 6 chars). + */ + val truncatedToAddress: String + get() = truncateAddress(toAddress) + + /** + * Truncated sender address for display (first 8 + last 6 chars). + */ + val truncatedFromAddress: String + get() = truncateAddress(fromAddress) + + /** + * CardanoScan URL for viewing the transaction. + */ + val explorerUrl: String? + get() = txHash?.let { hash -> + if (isTestnet) { + "https://preprod.cardanoscan.io/transaction/$hash" + } else { + "https://cardanoscan.io/transaction/$hash" + } + } + + companion object { + /** Custom event type for Cardano payment requests (reverse-domain format) */ + const val EVENT_TYPE = "co.sulkta.payment.request" + private const val LOVELACE_PER_ADA = 1_000_000.0 + + /** + * Format lovelace amount as ADA string. + */ + fun formatAda(lovelace: Long): String { + val ada = lovelace / LOVELACE_PER_ADA + return if (ada == ada.toLong().toDouble()) { + "${ada.toLong()} ADA" + } else { + val formatted = "%.6f".format(ada).trimEnd('0').trimEnd('.') + "$formatted ADA" + } + } + + /** + * Truncate a Cardano address for display (first 8 + last 6 chars). + */ + fun truncateAddress(address: String): String { + return if (address.length > 18) { + "${address.take(8)}...${address.takeLast(6)}" + } else { + address + } + } + } +} diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts new file mode 100644 index 0000000000..3ce12b70a6 --- /dev/null +++ b/features/wallet/impl/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import extension.setupDependencyInjection + +plugins { + id("io.element.android-compose-library") + id("kotlin-parcelize") + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "io.element.android.features.wallet.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +setupDependencyInjection() + +dependencies { + api(projects.features.wallet.api) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrix.impl) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.cryptography.api) + implementation(projects.libraries.uiStrings) + + // Cardano - using Koios backend (no API key required) + implementation("com.bloxbean.cardano:cardano-client-lib:0.7.1") + implementation("com.bloxbean.cardano:cardano-client-backend-koios:0.7.1") + implementation("com.bloxbean.cardano:cardano-client-crypto:0.7.1") + + // Biometric + implementation(libs.androidx.biometric) + + // JSON + implementation(libs.serialization.json) + // QR code generation + implementation(libs.google.zxing) + + // Coroutines + implementation(libs.coroutines.core) + + // Image loading for NFT thumbnails + implementation(libs.coil.compose) + implementation(libs.coil.network.okhttp) + + // Testing + testImplementation(projects.features.wallet.test) + testImplementation(projects.libraries.matrix.test) + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.coroutines.test) + testImplementation(libs.test.turbine) +} diff --git a/features/wallet/impl/proguard-rules.pro b/features/wallet/impl/proguard-rules.pro new file mode 100644 index 0000000000..a520813972 --- /dev/null +++ b/features/wallet/impl/proguard-rules.pro @@ -0,0 +1,10 @@ +# Cardano client library uses reflection for CBOR serialization +-keep class com.bloxbean.cardano.** { *; } +-keepclassmembers class * { + @com.fasterxml.jackson.annotation.* *; +} + +# Keep the Cardano model classes +-keep class com.bloxbean.cardano.client.api.model.** { *; } +-keep class com.bloxbean.cardano.client.backend.model.** { *; } +-keep class com.bloxbean.cardano.client.transaction.spec.** { *; } diff --git a/features/wallet/impl/src/main/AndroidManifest.xml b/features/wallet/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0baf68a8a8 --- /dev/null +++ b/features/wallet/impl/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/DefaultWalletEntryPoint.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/DefaultWalletEntryPoint.kt new file mode 100644 index 0000000000..75eee7fdf5 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/DefaultWalletEntryPoint.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.features.wallet.api.WalletEntryPoint +import io.element.android.features.wallet.impl.slash.ParsedPayCommand +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId + +@ContributesBinding(SessionScope::class) +class DefaultWalletEntryPoint @Inject constructor() : WalletEntryPoint { + class Builder( + private val parentNode: Node, + private val buildContext: BuildContext, + private val callback: WalletEntryPoint.Callback, + ) : WalletEntryPoint.Builder { + private var roomId: RoomId? = null + private var recipientUserId: UserId? = null + private var recipientAddress: String? = null + private var amountLovelace: Long? = null + private var parsedCommand: ParsedPayCommand? = null + + override fun setRoomId(roomId: RoomId): Builder { + this.roomId = roomId + return this + } + + override fun setRecipientUserId(userId: UserId?): Builder { + this.recipientUserId = userId + return this + } + + override fun setRecipientAddress(address: String?): Builder { + this.recipientAddress = address + return this + } + + override fun setAmount(amount: String?): Builder { + this.amountLovelace = amount?.toLongOrNull()?.let { value -> + if (value < 1_000_000) { + value * 1_000_000 + } else { + value + } + } + return this + } + + fun setParsedCommand(command: ParsedPayCommand?): Builder { + this.parsedCommand = command + when (command) { + is ParsedPayCommand.WithAddressRecipient -> { + this.amountLovelace = command.amount + this.recipientAddress = command.address + } + is ParsedPayCommand.WithMatrixRecipient -> { + this.amountLovelace = command.amount + this.recipientUserId = command.matrixUserId + } + is ParsedPayCommand.AmountOnly -> { + this.amountLovelace = command.amount + } + else -> Unit + } + return this + } + + override fun build(): Node { + val inputs = PaymentFlowNode.Inputs( + roomId = requireNotNull(roomId) { "roomId must be set" }, + recipientUserId = recipientUserId, + recipientAddress = recipientAddress, + amountLovelace = amountLovelace, + parsedCommand = parsedCommand, + ) + return parentNode.createNode(buildContext, listOf(inputs, callback)) + } + } + + override fun paymentFlowBuilder( + parentNode: Node, + buildContext: BuildContext, + callback: WalletEntryPoint.Callback, + ): WalletEntryPoint.Builder { + return Builder(parentNode, buildContext, callback) + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/PaymentFlowNode.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/PaymentFlowNode.kt new file mode 100644 index 0000000000..260da6da4d --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/PaymentFlowNode.kt @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop +import com.bumble.appyx.navmodel.backstack.operation.push +import com.bumble.appyx.navmodel.backstack.operation.replace +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.wallet.api.WalletEntryPoint +import io.element.android.features.wallet.impl.payment.PaymentConfirmationNode +import io.element.android.features.wallet.impl.payment.PaymentEntryNode +import io.element.android.features.wallet.impl.payment.PaymentProgressNode +import io.element.android.features.wallet.impl.slash.Lovelace +import io.element.android.features.wallet.impl.slash.ParsedPayCommand +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.parcelize.Parcelize + +/** + * Main flow node for the payment flow. + * + * Navigation flow: + * 1. Entry (amount/recipient input) + * 2. Confirmation (tx details, biometric auth) + * 3. Progress (submission + polling) + * + * Can skip to Confirmation if all details are pre-filled from /pay command. + */ +@ContributesNode(SessionScope::class) +@AssistedInject +class PaymentFlowNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : BaseFlowNode( + backstack = BackStack( + initialElement = initialElementFromInputs(plugins.filterIsInstance().first()), + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { + @Parcelize + data class Inputs( + val roomId: RoomId, + val recipientUserId: UserId?, + val recipientAddress: String?, + val amountLovelace: Lovelace?, + val parsedCommand: ParsedPayCommand?, + ) : NodeInputs, Parcelable + + private val callback: WalletEntryPoint.Callback = callback() + private val inputs: Inputs = plugins.filterIsInstance().first() + + sealed interface NavTarget : Parcelable { + @Parcelize + data class Entry( + val roomId: RoomId, + val parsedCommand: ParsedPayCommand?, + ) : NavTarget + + @Parcelize + data class Confirmation( + val recipientAddress: String, + val amountLovelace: Lovelace, + val assetPolicyId: String?, + val assetName: String?, + val assetQuantity: Long?, + val assetDisplayName: String?, + ) : NavTarget + + @Parcelize + data class Progress( + val recipientAddress: String, + val amountLovelace: Lovelace, + val roomId: RoomId, + val assetPolicyId: String?, + val assetName: String?, + val assetQuantity: Long?, + val assetDisplayName: String?, + ) : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.Entry -> { + val nodeInputs = PaymentEntryNode.Inputs( + roomId = navTarget.roomId, + parsedCommand = navTarget.parsedCommand, + ) + val nodeCallback = object : PaymentEntryNode.Callback { + override fun onContinue( + recipientAddress: String, + amountLovelace: Long, + assetPolicyId: String?, + assetName: String?, + assetQuantity: Long?, + assetDisplayName: String?, + ) { + backstack.push(NavTarget.Confirmation( + recipientAddress = recipientAddress, + amountLovelace = amountLovelace, + assetPolicyId = assetPolicyId, + assetName = assetName, + assetQuantity = assetQuantity, + assetDisplayName = assetDisplayName, + )) + } + + override fun onCancel() { + callback.onPaymentCancelled() + } + + override fun onOpenWalletSettings() { + // Cancel the payment flow and request wallet settings to be opened + callback.onOpenWalletSettings() + } + } + createNode(buildContext, plugins = listOf(nodeInputs, nodeCallback)) + } + + is NavTarget.Confirmation -> { + val nodeInputs = PaymentConfirmationNode.Inputs( + recipientAddress = navTarget.recipientAddress, + amountLovelace = navTarget.amountLovelace, + assetPolicyId = navTarget.assetPolicyId, + assetName = navTarget.assetName, + assetQuantity = navTarget.assetQuantity, + assetDisplayName = navTarget.assetDisplayName, + ) + val nodeCallback = object : PaymentConfirmationNode.Callback { + override fun onConfirmed() { + backstack.replace(NavTarget.Progress( + recipientAddress = navTarget.recipientAddress, + amountLovelace = navTarget.amountLovelace, + roomId = inputs.roomId, + assetPolicyId = navTarget.assetPolicyId, + assetName = navTarget.assetName, + assetQuantity = navTarget.assetQuantity, + assetDisplayName = navTarget.assetDisplayName, + )) + } + + override fun onBack() { + backstack.pop() + } + } + createNode(buildContext, plugins = listOf(nodeInputs, nodeCallback)) + } + + is NavTarget.Progress -> { + val nodeInputs = PaymentProgressNode.Inputs( + recipientAddress = navTarget.recipientAddress, + amountLovelace = navTarget.amountLovelace, + roomId = navTarget.roomId, + assetPolicyId = navTarget.assetPolicyId, + assetName = navTarget.assetName, + assetQuantity = navTarget.assetQuantity, + assetDisplayName = navTarget.assetDisplayName, + ) + val nodeCallback = object : PaymentProgressNode.Callback { + override fun onPaymentComplete(txHash: String?) { + if (txHash != null) { + callback.onPaymentSent(txHash) + } else { + callback.onPaymentCancelled() + } + } + + override fun onRetry() { + // Go back to entry to retry + backstack.pop() + backstack.pop() + } + } + createNode(buildContext, plugins = listOf(nodeInputs, nodeCallback)) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView() + } +} + +/** + * Determines the initial screen based on the inputs. + * + * If we have all required data (amount + valid address), skip to confirmation. + * Otherwise, show entry screen. + */ +private fun initialElementFromInputs(inputs: PaymentFlowNode.Inputs): PaymentFlowNode.NavTarget { + // Check if we can skip to confirmation + val parsedCommand = inputs.parsedCommand + if (parsedCommand is ParsedPayCommand.WithAddressRecipient) { + // Have both amount and address - go directly to confirmation (ADA only) + return PaymentFlowNode.NavTarget.Confirmation( + recipientAddress = parsedCommand.address, + amountLovelace = parsedCommand.amount, + assetPolicyId = null, + assetName = null, + assetQuantity = null, + assetDisplayName = null, + ) + } + + // If we have a direct address and amount in inputs + if (inputs.recipientAddress != null && inputs.amountLovelace != null) { + return PaymentFlowNode.NavTarget.Confirmation( + recipientAddress = inputs.recipientAddress, + amountLovelace = inputs.amountLovelace, + assetPolicyId = null, + assetName = null, + assetQuantity = null, + assetDisplayName = null, + ) + } + + // Default: show entry screen + return PaymentFlowNode.NavTarget.Entry( + roomId = inputs.roomId, + parsedCommand = inputs.parsedCommand, + ) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/address/DefaultCardanoAddressService.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/address/DefaultCardanoAddressService.kt new file mode 100644 index 0000000000..5f6ccc3d84 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/address/DefaultCardanoAddressService.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.address + +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.features.wallet.api.address.CardanoAddressService +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.sessionstorage.api.SessionStore +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import timber.log.Timber +import java.util.concurrent.TimeUnit + +/** + * Implementation of [CardanoAddressService] that stores Cardano addresses + * in Matrix account data for public discovery. + */ +@ContributesBinding(SessionScope::class) +class DefaultCardanoAddressService @Inject constructor( + private val matrixClient: MatrixClient, + private val sessionStore: SessionStore, + private val dispatchers: CoroutineDispatchers, +) : CardanoAddressService { + + private val json = Json { + ignoreUnknownKeys = true + encodeDefaults = true + } + + private val httpClient: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + } + + @Serializable + private data class CardanoAddressData( + val address: String + ) + + override suspend fun publishAddress(address: String): Result = withContext(dispatchers.io) { + runCatching { + val sessionData = sessionStore.getSession(matrixClient.sessionId.value) + ?: throw IllegalStateException("No session found") + + val userId = matrixClient.sessionId.value + val url = "${sessionData.homeserverUrl}/_matrix/client/v3/user/$userId/account_data/${CardanoAddressService.ACCOUNT_DATA_TYPE}" + + val body = json.encodeToString(CardanoAddressData(address)) + .toRequestBody("application/json".toMediaType()) + + val request = Request.Builder() + .url(url) + .put(body) + .addHeader("Authorization", "Bearer ${sessionData.accessToken}") + .build() + + Timber.d("Publishing Cardano address to Matrix account data...") + + val response = httpClient.newCall(request).execute() + if (!response.isSuccessful) { + val errorBody = response.body?.string() ?: "Unknown error" + throw RuntimeException("Failed to publish address: ${response.code} - $errorBody") + } + + Timber.i("Successfully published Cardano address to Matrix account data") + } + } + + override suspend fun lookupAddress(userId: UserId): Result = withContext(dispatchers.io) { + runCatching { + val sessionData = sessionStore.getSession(matrixClient.sessionId.value) + ?: throw IllegalStateException("No session found") + + val url = "${sessionData.homeserverUrl}/_matrix/client/v3/user/${userId.value}/account_data/${CardanoAddressService.ACCOUNT_DATA_TYPE}" + + Timber.d("Looking up Cardano address for ${userId.value}...") + + val request = Request.Builder() + .url(url) + .get() + .addHeader("Authorization", "Bearer ${sessionData.accessToken}") + .build() + + val response = httpClient.newCall(request).execute() + + when (response.code) { + 200 -> { + val responseBody = response.body?.string() + if (responseBody != null) { + val data = json.decodeFromString(responseBody) + Timber.i("Found Cardano address for ${userId.value}: ${data.address.take(20)}...") + data.address + } else { + null + } + } + 404 -> { + Timber.d("No Cardano address found for ${userId.value}") + null + } + else -> { + val errorBody = response.body?.string() ?: "Unknown error" + throw RuntimeException("Failed to lookup address: ${response.code} - $errorBody") + } + } + } + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/backup/WalletBackupServiceImpl.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/backup/WalletBackupServiceImpl.kt new file mode 100644 index 0000000000..9f97baaff4 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/backup/WalletBackupServiceImpl.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.backup + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.features.wallet.api.backup.WalletBackupService +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.walletsecretstorage.WalletSecretStorage +import io.element.android.libraries.matrix.api.walletsecretstorage.WalletSecretStorageException +import timber.log.Timber + +/** + * [WalletBackupService] implementation that stores the Cardano wallet + * seed phrase encrypted in Matrix account data via [WalletSecretStorage]. + * + * We persist the mnemonic as a single space-separated string — the wire + * form of the seed most BIP-39 tools already accept. The backup service + * re-splits on read. + * + * History note: prior to 2026-04, this class used a low-level + * `SecretStoreWrapper.putSecret` API that the Rust SDK removed between + * 26.03.24 and 26.04.x. The new path uses Matrix account data under our + * own namespace with our own AES-256-GCM envelope, so we no longer depend + * on any SDK-internal secret-storage primitive. + */ +@ContributesBinding(AppScope::class) +class WalletBackupServiceImpl @Inject constructor( + private val matrixClient: MatrixClient, +) : WalletBackupService { + + private val storage: WalletSecretStorage + get() = matrixClient.walletSecretStorage + + override suspend fun backupSeed(recoveryKey: String, mnemonic: List): Result { + val seedString = mnemonic.joinToString(" ") + return storage.putSeed(recoveryKey, seedString) + .onSuccess { Timber.d("[WalletBackup] seed stored in account data") } + .onFailure { Timber.w(it, "[WalletBackup] seed storage failed") } + } + + override suspend fun restoreSeed(recoveryKey: String): Result?> { + return storage.getSeed(recoveryKey).map { seedString -> + seedString?.split(" ")?.takeIf { it.size in VALID_MNEMONIC_LENGTHS } + } + } + + override suspend fun hasBackup(recoveryKey: String): Result { + // A successful decrypt into a valid-length mnemonic is our criterion. + // Distinguishes "blob exists but wrong key" from "blob exists and opens". + return restoreSeed(recoveryKey).map { it != null } + } + + override suspend fun hasBackupWithoutKey(): Result { + return storage.hasSeedBackup() + .onFailure { Timber.w(it, "[WalletBackup] hasSeedBackup probe failed") } + } + + private companion object { + /** BIP-39 permits these mnemonic word counts; anything else is corrupt. */ + val VALID_MNEMONIC_LENGTHS = setOf(12, 15, 18, 21, 24) + } +} + +/** + * Exceptions surfaced by wallet backup operations. Kept for compatibility + * with call sites that pattern-match; the underlying storage failures now + * come from [WalletSecretStorageException]. + */ +sealed class WalletBackupException(message: String) : Exception(message) { + class InvalidRecoveryKey : WalletBackupException("Recovery key is invalid or could not unlock the backup") + class NoBackupFound : WalletBackupException("No wallet backup found") +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/biometric/BiometricAuthenticator.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/biometric/BiometricAuthenticator.kt new file mode 100644 index 0000000000..e8b29e317d --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/biometric/BiometricAuthenticator.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.biometric + +import android.content.Context +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +/** + * Helper class for biometric authentication at transaction signing. + * + * Uses BIOMETRIC_WEAK | DEVICE_CREDENTIAL to support: + * - Fingerprint/face → biometric prompt + * - PIN only → PIN prompt + * - No auth set up → skips auth (doesn't block transactions) + */ +class BiometricAuthenticator @Inject constructor() { + + sealed interface AuthResult { + data object Success : AuthResult + data class Error(val code: Int, val message: String) : AuthResult + data object Cancelled : AuthResult + } + + /** + * Check if any authentication method is available. + * Returns true if biometric OR device credential (PIN/pattern/password) is available. + */ + fun canAuthenticate(context: Context): Boolean { + val biometricManager = BiometricManager.from(context) + val result = biometricManager.canAuthenticate( + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + return result == BiometricManager.BIOMETRIC_SUCCESS + } + + /** + * Check if device has any form of security (biometric, PIN, pattern, password). + * If false, authentication will be skipped to avoid blocking transactions. + */ + fun isDeviceSecured(context: Context): Boolean { + val biometricManager = BiometricManager.from(context) + // Check both weak biometric and device credential + val weakResult = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) + val credentialResult = biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + return weakResult == BiometricManager.BIOMETRIC_SUCCESS || + credentialResult == BiometricManager.BIOMETRIC_SUCCESS + } + + /** + * Authenticate the user before a sensitive action (e.g., signing a transaction). + * + * - If device has biometric → shows biometric prompt + * - If device has only PIN/pattern/password → shows device credential prompt + * - If device has no security → returns Success immediately (don't block the tx) + */ + suspend fun authenticate( + activity: FragmentActivity, + title: String = "Confirm Payment", + subtitle: String = "Authenticate to send ADA", + ): AuthResult { + // If device has no security set up, allow through + if (!isDeviceSecured(activity)) { + return AuthResult.Success + } + + return suspendCancellableCoroutine { continuation -> + val executor = ContextCompat.getMainExecutor(activity) + + val callback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + if (continuation.isActive) { + continuation.resume(AuthResult.Success) + } + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + if (continuation.isActive) { + when (errorCode) { + BiometricPrompt.ERROR_USER_CANCELED, + BiometricPrompt.ERROR_NEGATIVE_BUTTON, + BiometricPrompt.ERROR_CANCELED -> { + continuation.resume(AuthResult.Cancelled) + } + else -> { + continuation.resume(AuthResult.Error(errorCode, errString.toString())) + } + } + } + } + + override fun onAuthenticationFailed() { + // User can retry, don't complete the continuation + } + } + + val biometricPrompt = BiometricPrompt(activity, executor, callback) + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(title) + .setSubtitle(subtitle) + .setAllowedAuthenticators( + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + .build() + + biometricPrompt.authenticate(promptInfo) + + continuation.invokeOnCancellation { + biometricPrompt.cancelAuthentication() + } + } + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/CardanoNetworkConfig.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/CardanoNetworkConfig.kt new file mode 100644 index 0000000000..46374fb669 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/CardanoNetworkConfig.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.cardano + +/** + * Cardano network type. + */ +enum class CardanoNetwork { + TESTNET, + MAINNET, +} + +/** + * Centralized network configuration for the Cardano wallet. + * + * To switch networks, change [NETWORK] to [CardanoNetwork.TESTNET]. + * All derived values (network ID, API URLs) will update automatically. + * + * **Current configuration: MAINNET** + */ +object CardanoNetworkConfig { + /** + * ⚠️ SWAP THIS VALUE TO SWITCH NETWORKS ⚠️ + * + * Set to [CardanoNetwork.TESTNET] for development/testing. + * Set to [CardanoNetwork.MAINNET] for production. + */ + val NETWORK: CardanoNetwork = CardanoNetwork.MAINNET + + /** + * Cardano network ID. + * - Testnet (preprod): 0 + * - Mainnet: 1 + */ + val NETWORK_ID: Int = when (NETWORK) { + CardanoNetwork.TESTNET -> 0 + CardanoNetwork.MAINNET -> 1 + } + + /** + * Koios API base URL for the configured network. + * Koios is a decentralized API layer for Cardano requiring no API key. + * + * Rate limits: 100 req/10s for anonymous users. + */ + val KOIOS_BASE_URL: String = when (NETWORK) { + CardanoNetwork.TESTNET -> "https://preprod.koios.rest/api/v1/" + CardanoNetwork.MAINNET -> "https://api.koios.rest/api/v1/" + } + + /** + * CardanoScan explorer URL for viewing transactions. + */ + val EXPLORER_BASE_URL: String = when (NETWORK) { + CardanoNetwork.TESTNET -> "https://preprod.cardanoscan.io" + CardanoNetwork.MAINNET -> "https://cardanoscan.io" + } + + /** + * Bech32 address prefix for the configured network. + */ + val ADDRESS_PREFIX: String = when (NETWORK) { + CardanoNetwork.TESTNET -> "addr_test1" + CardanoNetwork.MAINNET -> "addr1" + } + + /** + * Human-readable network name. + */ + val NETWORK_NAME: String = when (NETWORK) { + CardanoNetwork.TESTNET -> "Preprod Testnet" + CardanoNetwork.MAINNET -> "Mainnet" + } + + /** + * Returns the Network instance for cardano-client-lib. + */ + fun getNetwork(): com.bloxbean.cardano.client.common.model.Network = when (NETWORK) { + CardanoNetwork.TESTNET -> com.bloxbean.cardano.client.common.model.Networks.preprod() + CardanoNetwork.MAINNET -> com.bloxbean.cardano.client.common.model.Networks.mainnet() + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManager.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManager.kt new file mode 100644 index 0000000000..bb4178a8be --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManager.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.cardano + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.features.wallet.api.WalletState +import io.element.android.features.wallet.api.storage.CardanoKeyStorage +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber + +interface CardanoWalletManager { + val walletState: StateFlow + suspend fun initialize(sessionId: SessionId) + suspend fun getAddress(sessionId: SessionId): Result + suspend fun getStakeAddress(sessionId: SessionId): Result + /** Called by session-scoped components after fetching balance from chain. */ + suspend fun refreshBalance(sessionId: SessionId, balanceLovelace: Long) + suspend fun getMnemonic(sessionId: SessionId): Result> + fun clearState() +} + +/** + * App-scoped wallet manager. Handles key derivation and state only. + * Balance refresh is driven by session-scoped components that have access to CardanoClient. + */ +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultCardanoWalletManager @Inject constructor( + private val keyStorage: CardanoKeyStorage, +) : CardanoWalletManager { + + private val _walletState = MutableStateFlow(WalletState.Initial) + override val walletState: StateFlow = _walletState + + override suspend fun initialize(sessionId: SessionId) { + _walletState.value = WalletState.Initial.copy(isLoading = true) + try { + val hasWallet = keyStorage.hasWallet(sessionId) + if (hasWallet) { + val address = keyStorage.getBaseAddress(sessionId).getOrNull() + _walletState.value = WalletState( + isLoading = false, + hasWallet = true, + address = address, + balanceLovelace = 0L, + balanceAda = "0", + error = null, + ) + } else { + _walletState.value = WalletState( + isLoading = false, + hasWallet = false, + address = null, + balanceLovelace = null, + balanceAda = null, + error = null, + ) + } + } catch (e: Exception) { + Timber.e(e, "Failed to initialize wallet") + _walletState.value = WalletState( + isLoading = false, + hasWallet = false, + address = null, + balanceLovelace = null, + balanceAda = null, + error = e.message, + ) + } + } + + override suspend fun getAddress(sessionId: SessionId): Result = + keyStorage.getBaseAddress(sessionId) + + override suspend fun getStakeAddress(sessionId: SessionId): Result = + keyStorage.getStakeAddress(sessionId) + + override suspend fun refreshBalance(sessionId: SessionId, balanceLovelace: Long) { + val current = _walletState.value + if (current.hasWallet) { + val ada = "%.6f".format(balanceLovelace / 1_000_000.0) + _walletState.value = current.copy( + balanceLovelace = balanceLovelace, + balanceAda = ada, + isLoading = false, + ) + } + } + + override suspend fun getMnemonic(sessionId: SessionId): Result> = keyStorage.getMnemonic(sessionId) + override fun clearState() { + _walletState.value = WalletState.Initial + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/DefaultTransactionBuilder.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/DefaultTransactionBuilder.kt new file mode 100644 index 0000000000..2ee165112c --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/DefaultTransactionBuilder.kt @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.cardano + +import com.bloxbean.cardano.client.account.Account +import com.bloxbean.cardano.client.api.model.Amount +import com.bloxbean.cardano.client.backend.api.BackendService +import com.bloxbean.cardano.client.backend.koios.KoiosBackendService +import com.bloxbean.cardano.client.function.helper.SignerProviders +import com.bloxbean.cardano.client.quicktx.QuickTxBuilder +import com.bloxbean.cardano.client.quicktx.Tx +import com.bloxbean.cardano.client.transaction.util.TransactionUtil +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.features.wallet.api.CardanoClient +import io.element.android.features.wallet.api.CardanoException +import io.element.android.features.wallet.api.PaymentRequest +import io.element.android.features.wallet.api.SignedTransaction +import io.element.android.features.wallet.api.TransactionBuilder +import io.element.android.features.wallet.api.storage.CardanoKeyStorage +import io.element.android.libraries.di.SessionScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.math.BigInteger + +/** + * Default implementation of [TransactionBuilder] using cardano-client-lib. + */ +@ContributesBinding(SessionScope::class) +class DefaultTransactionBuilder @Inject constructor( + private val cardanoClient: CardanoClient, + private val keyStorage: CardanoKeyStorage, +) : TransactionBuilder { + + companion object { + private const val TAG = "TransactionBuilder" + const val MIN_UTXO_LOVELACE = 1_000_000L + // Minimum ADA to include with token sends (protocol requirement) + const val MIN_TOKEN_UTXO_LOVELACE = 1_500_000L + private const val ROUGH_FEE_ESTIMATE = 200_000L + } + + private val backendService: BackendService by lazy { + Timber.tag(TAG).d("Initializing Koios backend for tx building") + KoiosBackendService(CardanoNetworkConfig.KOIOS_BASE_URL) + } + + override suspend fun buildAndSign(request: PaymentRequest): Result = withContext(Dispatchers.IO) { + Timber.tag(TAG).d("Building transaction: ${request.amountLovelace} lovelace to ${request.toAddress.take(20)}...") + if (request.hasAsset) { + Timber.tag(TAG).d("Including asset: ${request.assetPolicyId?.take(16)}... qty=${request.assetQuantity}") + } + + runCatching { + validateAddress(request.fromAddress, "sender") + validateAddress(request.toAddress, "recipient") + + // For token sends, enforce minimum ADA + val effectiveLovelace = if (request.hasAsset) { + maxOf(request.amountLovelace, MIN_TOKEN_UTXO_LOVELACE) + } else { + request.amountLovelace + } + + if (!request.hasAsset && effectiveLovelace < MIN_UTXO_LOVELACE) { + throw CardanoException.ApiException( + message = "Amount too small: minimum is 1 ADA (1,000,000 lovelace)", + response = "MIN_UTXO_VIOLATION" + ) + } + + val utxos = cardanoClient.getUtxos(request.fromAddress).getOrThrow() + if (utxos.isEmpty()) { + throw CardanoException.InsufficientFundsException( + required = effectiveLovelace, + available = 0L + ) + } + + val totalAvailable = utxos.sumOf { it.amount } + val estimatedRequired = effectiveLovelace + ROUGH_FEE_ESTIMATE + + if (totalAvailable < estimatedRequired) { + throw CardanoException.InsufficientFundsException( + required = estimatedRequired, + available = totalAvailable + ) + } + + // Validate token balance if sending tokens + if (request.hasAsset) { + val availableTokens = utxos.flatMap { it.assets } + .filter { it.policyId == request.assetPolicyId && it.assetName == request.assetName } + .sumOf { it.quantity } + + if (availableTokens < (request.assetQuantity ?: 0L)) { + throw CardanoException.ApiException( + message = "Insufficient token balance: have $availableTokens, need ${request.assetQuantity}", + response = "INSUFFICIENT_TOKEN_BALANCE" + ) + } + } + + Timber.tag(TAG).d("UTXOs: ${utxos.size} totaling $totalAvailable lovelace") + + val mnemonicWords = keyStorage.getMnemonic(request.sessionId).getOrThrow() + val mnemonicString = mnemonicWords.joinToString(" ") + + try { + val signedTx = buildTransaction( + senderAddress = request.fromAddress, + recipientAddress = request.toAddress, + amountLovelace = effectiveLovelace, + mnemonic = mnemonicString, + assetPolicyId = request.assetPolicyId, + assetName = request.assetName, + assetQuantity = request.assetQuantity, + ) + + Timber.tag(TAG).i("Transaction built: ${signedTx.txHash}, fee: ${signedTx.fee} lovelace") + signedTx + } finally { + Timber.tag(TAG).d("Transaction building complete") + } + } + } + + private fun buildTransaction( + senderAddress: String, + recipientAddress: String, + amountLovelace: Long, + mnemonic: String, + assetPolicyId: String? = null, + assetName: String? = null, + assetQuantity: Long? = null, + ): SignedTransaction { + val account = Account(CardanoNetworkConfig.getNetwork(), mnemonic) + + // Build the list of amounts to send + val amounts = mutableListOf() + + // Always include ADA + amounts.add(Amount.lovelace(BigInteger.valueOf(amountLovelace))) + + // Add native asset if specified + if (assetPolicyId != null && assetName != null && assetQuantity != null && assetQuantity > 0) { + amounts.add(Amount.asset(assetPolicyId, assetName, BigInteger.valueOf(assetQuantity))) + } + + val tx = Tx() + .payToAddress(recipientAddress, amounts) + .from(senderAddress) + + val quickTxBuilder = QuickTxBuilder(backendService) + + val signedTx = quickTxBuilder + .compose(tx) + .withSigner(SignerProviders.signerFrom(account)) + .buildAndSign() + + val txHash = TransactionUtil.getTxHash(signedTx) + val txCbor = signedTx.serializeToHex() + val fee = signedTx.body.fee.toLong() + + return SignedTransaction( + txCbor = txCbor, + txHash = txHash, + fee = fee, + actualAmount = amountLovelace, + ) + } + + private fun validateAddress(address: String, role: String) { + val expectedPrefix = CardanoNetworkConfig.ADDRESS_PREFIX + + if (!address.startsWith(expectedPrefix)) { + throw CardanoException.InvalidAddressException(address) + } + + if (address.length < 50) { + throw CardanoException.InvalidAddressException(address) + } + + Timber.tag(TAG).d("$role address validated: ${address.take(20)}...") + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/KoiosCardanoClient.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/KoiosCardanoClient.kt new file mode 100644 index 0000000000..466f496e20 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/KoiosCardanoClient.kt @@ -0,0 +1,658 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.cardano + +import com.bloxbean.cardano.client.backend.api.BackendService +import com.bloxbean.cardano.client.backend.koios.KoiosBackendService +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.features.wallet.api.CardanoClient +import io.element.android.features.wallet.api.CardanoException +import io.element.android.features.wallet.api.NativeAsset +import io.element.android.features.wallet.api.NftMetadata +import io.element.android.features.wallet.api.ProtocolParameters +import io.element.android.features.wallet.api.TxStatus +import io.element.android.features.wallet.api.TxSummary +import io.element.android.features.wallet.api.Utxo +import io.element.android.features.wallet.api.UtxoAsset +import io.element.android.libraries.di.SessionScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONArray +import org.json.JSONObject +import timber.log.Timber +import java.util.concurrent.TimeUnit + +/** + * Cardano blockchain client using the Koios public API. + * Uses direct HTTP calls for reliable API compatibility. + */ +@ContributesBinding(SessionScope::class) +class KoiosCardanoClient @Inject constructor() : CardanoClient { + companion object { + private const val TAG = "KoiosCardanoClient" + private const val MAX_RETRIES = 3 + private const val INITIAL_BACKOFF_MS = 1000L + private const val MAX_BACKOFF_MS = 10000L + private const val MIN_REQUEST_INTERVAL_MS = 100L + private val JSON_MEDIA_TYPE = "application/json".toMediaType() + + // ADA Handle policy ID (same for mainnet and testnet) + private const val ADA_HANDLE_POLICY_ID = "f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a" + private const val HANDLE_CACHE_TTL_MS = 60 * 60 * 1000L // 1 hour + } + + private val httpClient: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + } + + // Fallback to cardano-client-lib for protocol params and tx submission + private val backendService: BackendService by lazy { + Timber.tag(TAG).d("Initializing Koios backend for ${CardanoNetworkConfig.NETWORK_NAME}") + KoiosBackendService(CardanoNetworkConfig.KOIOS_BASE_URL) + } + + private val rateLimitMutex = Mutex() + private var lastRequestTimeMs = 0L + + // Handle resolution cache + private data class CachedHandle(val address: String?, val timestamp: Long) + private val handleCache = mutableMapOf() + + // NFT metadata cache + private val nftMetadataCache = mutableMapOf() + + override suspend fun getBalance(address: String): Result = + withRetry("getBalance($address)") { + withContext(Dispatchers.IO) { + throttleRequest() + + // Use direct HTTP POST to Koios address_info endpoint + val url = "${CardanoNetworkConfig.KOIOS_BASE_URL}address_info" + val body = JSONObject().apply { + put("_addresses", JSONArray().put(address)) + }.toString() + + Timber.tag(TAG).d("getBalance calling: $url") + + val request = Request.Builder() + .url(url) + .post(body.toRequestBody(JSON_MEDIA_TYPE)) + .header("Accept", "application/json") + .build() + + val response = httpClient.newCall(request).execute() + val responseBody = response.body?.string() ?: "" + + Timber.tag(TAG).d("getBalance response: code=${response.code}, body=${responseBody.take(500)}") + + if (!response.isSuccessful) { + return@withContext Result.failure(parseHttpError(response.code, responseBody)) + } + + // Parse JSON response + val jsonArray = JSONArray(responseBody) + if (jsonArray.length() == 0) { + // No data means unfunded address + Timber.tag(TAG).d("Address not found in response, returning 0") + return@withContext Result.success(0L) + } + + val addressInfo = jsonArray.getJSONObject(0) + val balance = addressInfo.optString("balance", "0").toLongOrNull() ?: 0L + Timber.tag(TAG).d("getBalance result: $balance lovelace") + Result.success(balance) + } + } + + override suspend fun getUtxos(address: String): Result> = + withRetry("getUtxos($address)") { + withContext(Dispatchers.IO) { + throttleRequest() + + // Use direct HTTP POST to Koios address_info endpoint (includes utxo_set) + val url = "${CardanoNetworkConfig.KOIOS_BASE_URL}address_info" + val body = JSONObject().apply { + put("_addresses", JSONArray().put(address)) + }.toString() + + Timber.tag(TAG).d("getUtxos calling: $url") + + val request = Request.Builder() + .url(url) + .post(body.toRequestBody(JSON_MEDIA_TYPE)) + .header("Accept", "application/json") + .build() + + val response = httpClient.newCall(request).execute() + val responseBody = response.body?.string() ?: "" + + if (!response.isSuccessful) { + return@withContext Result.failure(parseHttpError(response.code, responseBody)) + } + + val jsonArray = JSONArray(responseBody) + if (jsonArray.length() == 0) { + return@withContext Result.success(emptyList()) + } + + val addressInfo = jsonArray.getJSONObject(0) + val utxoSet = addressInfo.optJSONArray("utxo_set") ?: JSONArray() + + val utxos = (0 until utxoSet.length()).map { i -> + val utxoJson = utxoSet.getJSONObject(i) + val lovelace = utxoJson.optString("value", "0").toLongOrNull() ?: 0L + + // Parse native assets in this UTXO + val assetList = utxoJson.optJSONArray("asset_list") ?: JSONArray() + val assets = (0 until assetList.length()).map { j -> + val asset = assetList.getJSONObject(j) + UtxoAsset( + policyId = asset.getString("policy_id"), + assetName = asset.optString("asset_name", ""), + quantity = asset.optString("quantity", "0").toLongOrNull() ?: 0L, + ) + } + + Utxo( + txHash = utxoJson.getString("tx_hash"), + outputIndex = utxoJson.getInt("tx_index"), + amount = lovelace, + address = address, + assets = assets, + ) + } + + val totalAssets = utxos.flatMap { it.assets }.sumOf { it.quantity } + Timber.tag(TAG).d("getUtxos result: ${utxos.size} UTXOs, total=${utxos.sumOf { it.amount }}, assets=$totalAssets") + Result.success(utxos) + } + } + + override suspend fun submitTx(signedTxCbor: String): Result = + withRetry("submitTx") { + withContext(Dispatchers.IO) { + throttleRequest() + + val txBytes = try { + signedTxCbor.hexToByteArray() + } catch (e: Exception) { + return@withContext Result.failure( + CardanoException.SubmissionFailedException( + message = "Invalid CBOR hex string", + cause = e, + ) + ) + } + + val result = backendService.transactionService.submitTransaction(txBytes) + if (result.isSuccessful) { + Timber.tag(TAG).i("Transaction submitted: ${result.value}") + Result.success(result.value) + } else { + Timber.tag(TAG).e("Transaction submission failed: ${result.response}") + Result.failure( + CardanoException.SubmissionFailedException( + message = "Transaction submission failed", + errorCode = result.response, + ) + ) + } + } + } + + override suspend fun getTxStatus(txHash: String): Result = + withRetry("getTxStatus($txHash)") { + withContext(Dispatchers.IO) { + throttleRequest() + + val result = backendService.transactionService.getTransaction(txHash) + if (result.isSuccessful) { + Result.success(TxStatus.CONFIRMED) + } else { + val response = result.response ?: "" + when { + response.contains("404") || response.contains("not found", ignoreCase = true) -> { + Result.success(TxStatus.PENDING) + } + else -> { + Result.failure(parseError(response)) + } + } + } + } + } + + override suspend fun getProtocolParameters(): Result = + withRetry("getProtocolParameters") { + withContext(Dispatchers.IO) { + throttleRequest() + + val result = backendService.epochService.protocolParameters + if (result.isSuccessful) { + val params = result.value + Result.success( + ProtocolParameters( + minFeeA = params.minFeeA?.toLong() ?: 44L, + minFeeB = params.minFeeB?.toLong() ?: 155381L, + maxTxSize = params.maxTxSize ?: 16384, + utxoCostPerByte = params.coinsPerUtxoSize?.toLong() ?: 4310L, + ) + ) + } else { + Result.failure(parseError(result.response)) + } + } + } + + override suspend fun getAddressAssets(address: String): Result> = + withRetry("getAddressAssets($address)") { + withContext(Dispatchers.IO) { + throttleRequest() + + val url = "${CardanoNetworkConfig.KOIOS_BASE_URL}address_info" + val body = JSONObject().apply { + put("_addresses", JSONArray().put(address)) + }.toString() + + val request = Request.Builder() + .url(url) + .post(body.toRequestBody(JSON_MEDIA_TYPE)) + .header("Accept", "application/json") + .build() + + val response = httpClient.newCall(request).execute() + val responseBody = response.body?.string() ?: "" + + if (!response.isSuccessful) { + return@withContext Result.failure(parseHttpError(response.code, responseBody)) + } + + val jsonArray = JSONArray(responseBody) + if (jsonArray.length() == 0) { + return@withContext Result.success(emptyList()) + } + + val addressInfo = jsonArray.getJSONObject(0) + val utxoSet = addressInfo.optJSONArray("utxo_set") ?: JSONArray() + + val assetMap = mutableMapOf() + + for (i in 0 until utxoSet.length()) { + val utxoJson = utxoSet.getJSONObject(i) + val assetList = utxoJson.optJSONArray("asset_list") ?: continue + + for (j in 0 until assetList.length()) { + val asset = assetList.getJSONObject(j) + val policyId = asset.getString("policy_id") + val assetName = asset.optString("asset_name", "") + val quantity = asset.optString("quantity", "0").toLongOrNull() ?: 0L + val key = "$policyId$assetName" + assetMap[key] = (assetMap[key] ?: 0L) + quantity + } + } + + val assets = assetMap.map { (key, quantity) -> + val policyId = key.take(56) + val assetNameHex = key.drop(56) + // Mark as potential NFT if quantity is 1 + NativeAsset( + policyId = policyId, + assetName = assetNameHex, + quantity = quantity, + displayName = null, + fingerprint = null, + isNft = quantity == 1L, + ) + } + + Result.success(assets) + } + } + + override suspend fun getAddressTransactions(address: String, limit: Int): Result> = + withRetry("getAddressTransactions($address)") { + withContext(Dispatchers.IO) { + throttleRequest() + + val url = "${CardanoNetworkConfig.KOIOS_BASE_URL}address_txs" + val body = JSONObject().apply { + put("_addresses", JSONArray().put(address)) + }.toString() + + val request = Request.Builder() + .url(url) + .post(body.toRequestBody(JSON_MEDIA_TYPE)) + .header("Accept", "application/json") + .build() + + val response = httpClient.newCall(request).execute() + val responseBody = response.body?.string() ?: "" + + if (!response.isSuccessful) { + return@withContext Result.failure(parseHttpError(response.code, responseBody)) + } + + val jsonArray = JSONArray(responseBody) + val txs = (0 until minOf(jsonArray.length(), limit)).map { i -> + val txJson = jsonArray.getJSONObject(i) + TxSummary( + txHash = txJson.getString("tx_hash"), + blockTime = txJson.optLong("block_time", 0L), + totalOutput = 0L, + fee = 0L, + direction = TxSummary.Direction.RECEIVED, + ) + } + Result.success(txs) + } + } + + override suspend fun resolveHandle(handle: String): Result = + withRetry("resolveHandle($handle)") { + withContext(Dispatchers.IO) { + // Normalize handle to lowercase + val normalizedHandle = handle.lowercase().trim() + + // Check cache first + val cached = handleCache[normalizedHandle] + if (cached != null && System.currentTimeMillis() - cached.timestamp < HANDLE_CACHE_TTL_MS) { + Timber.tag(TAG).d("resolveHandle: cache hit for $normalizedHandle -> ${cached.address}") + return@withContext Result.success(cached.address) + } + + throttleRequest() + + // Convert handle to hex (ASCII bytes to hex string) + val handleHex = normalizedHandle.toByteArray(Charsets.US_ASCII) + .joinToString("") { "%02x".format(it) } + + val url = "${CardanoNetworkConfig.KOIOS_BASE_URL}asset_addresses" + val body = JSONObject().apply { + put("_asset_policy", ADA_HANDLE_POLICY_ID) + put("_asset_name", handleHex) + }.toString() + + Timber.tag(TAG).d("resolveHandle: $normalizedHandle -> hex=$handleHex, url=$url") + + val request = Request.Builder() + .url(url) + .post(body.toRequestBody(JSON_MEDIA_TYPE)) + .header("Accept", "application/json") + .build() + + val response = httpClient.newCall(request).execute() + val responseBody = response.body?.string() ?: "" + + Timber.tag(TAG).d("resolveHandle response: code=${response.code}, body=${responseBody.take(500)}") + + if (!response.isSuccessful) { + return@withContext Result.failure(parseHttpError(response.code, responseBody)) + } + + val jsonArray = JSONArray(responseBody) + val address = if (jsonArray.length() > 0) { + jsonArray.getJSONObject(0).getString("payment_address") + } else { + null + } + + // Cache the result + handleCache[normalizedHandle] = CachedHandle(address, System.currentTimeMillis()) + + Timber.tag(TAG).d("resolveHandle: $normalizedHandle -> $address") + Result.success(address) + } + } + + override suspend fun getNftMetadata(policyId: String, assetName: String): Result = + withRetry("getNftMetadata($policyId, $assetName)") { + withContext(Dispatchers.IO) { + val cacheKey = "$policyId$assetName" + + // Check cache first + if (nftMetadataCache.containsKey(cacheKey)) { + return@withContext Result.success(nftMetadataCache[cacheKey]) + } + + throttleRequest() + + val url = "${CardanoNetworkConfig.KOIOS_BASE_URL}asset_info" + val body = JSONObject().apply { + put("_asset_list", JSONArray().put(JSONArray().apply { + put(policyId) + put(assetName) + })) + }.toString() + + Timber.tag(TAG).d("getNftMetadata calling: $url with policy=$policyId, asset=$assetName") + + val request = Request.Builder() + .url(url) + .post(body.toRequestBody(JSON_MEDIA_TYPE)) + .header("Accept", "application/json") + .build() + + val response = httpClient.newCall(request).execute() + val responseBody = response.body?.string() ?: "" + + Timber.tag(TAG).d("getNftMetadata response: code=${response.code}, body=${responseBody.take(1000)}") + + if (!response.isSuccessful) { + return@withContext Result.failure(parseHttpError(response.code, responseBody)) + } + + val jsonArray = JSONArray(responseBody) + if (jsonArray.length() == 0) { + nftMetadataCache[cacheKey] = null + return@withContext Result.success(null) + } + + val assetInfo = jsonArray.getJSONObject(0) + + // Parse CIP-25 onchain_metadata + val metadata = try { + parseCip25Metadata(assetInfo) + } catch (e: Exception) { + Timber.tag(TAG).w(e, "Failed to parse CIP-25 metadata") + null + } + + nftMetadataCache[cacheKey] = metadata + Result.success(metadata) + } + } + + /** + * Parse CIP-25 metadata from Koios asset_info response. + */ + private fun parseCip25Metadata(assetInfo: JSONObject): NftMetadata? { + // Check for onchain_metadata (CIP-25) + val onchainMetadata = assetInfo.optJSONObject("onchain_metadata") ?: return null + + // Get asset name for lookup (decoded) + val assetNameHex = assetInfo.optString("asset_name", "") + val assetNameDecoded = assetInfo.optString("asset_name_ascii", "") + + // Extract name - could be in various places + val name = onchainMetadata.optString("name") + .takeIf { it.isNotEmpty() } + ?: assetNameDecoded.takeIf { it.isNotEmpty() } + ?: assetNameHex + + // Extract image - handle both string and array formats + val imageUrl = extractImageUrl(onchainMetadata) + + // Extract description + val description = onchainMetadata.optString("description") + .takeIf { it.isNotEmpty() } + + // Build raw metadata map + val rawMetadata = mutableMapOf() + onchainMetadata.keys().forEach { key -> + val value = onchainMetadata.get(key) + if (value != null && value != JSONObject.NULL) { + rawMetadata[key] = convertJsonValue(value) + } + } + + return NftMetadata( + name = name, + image = imageUrl, + description = description, + rawMetadata = rawMetadata, + ) + } + + /** + * Extract image URL from CIP-25 metadata, handling various formats. + */ + private fun extractImageUrl(metadata: JSONObject): String? { + return try { + when (val imageValue = metadata.opt("image")) { + is String -> NftMetadata.resolveImageUrl(imageValue) + is JSONArray -> { + // Some NFTs split the URL across multiple array elements + val parts = (0 until imageValue.length()).mapNotNull { + imageValue.optString(it).takeIf { s -> s.isNotEmpty() } + } + NftMetadata.joinImageParts(parts) + } + else -> null + } + } catch (e: Exception) { + Timber.tag(TAG).w(e, "Failed to extract image URL") + null + } + } + + /** + * Convert JSON value to Kotlin type for raw metadata map. + */ + private fun convertJsonValue(value: Any): Any { + return when (value) { + is JSONObject -> { + val map = mutableMapOf() + value.keys().forEach { key -> + val v = value.get(key) + if (v != null && v != JSONObject.NULL) { + map[key] = convertJsonValue(v) + } + } + map + } + is JSONArray -> { + (0 until value.length()).mapNotNull { i -> + val v = value.opt(i) + if (v != null && v != JSONObject.NULL) convertJsonValue(v) else null + } + } + else -> value + } + } + + private suspend fun withRetry( + operation: String, + block: suspend () -> Result, + ): Result { + var lastException: Throwable? = null + var backoffMs = INITIAL_BACKOFF_MS + + repeat(MAX_RETRIES) { attempt -> + Timber.tag(TAG).d("$operation: attempt ${attempt + 1}/$MAX_RETRIES") + + val result = try { + block() + } catch (e: Exception) { + Timber.tag(TAG).w(e, "$operation: exception on attempt ${attempt + 1}") + Result.failure(e) + } + + if (result.isSuccess) { + return result + } + + val exception = result.exceptionOrNull() ?: Exception("Unknown error") + lastException = exception + + val shouldRetry = when (exception) { + is CardanoException.RateLimitException -> { + backoffMs = exception.retryAfterMs ?: (backoffMs * 2).coerceAtMost(MAX_BACKOFF_MS) + true + } + is CardanoException.NetworkException -> { + exception.statusCode == null || exception.statusCode in 500..599 + } + else -> false + } + + if (!shouldRetry || attempt == MAX_RETRIES - 1) { + Timber.tag(TAG).e("$operation: giving up after ${attempt + 1} attempts") + return result + } + + Timber.tag(TAG).d("$operation: retrying in ${backoffMs}ms") + delay(backoffMs) + backoffMs = (backoffMs * 2).coerceAtMost(MAX_BACKOFF_MS) + } + + return Result.failure(lastException ?: Exception("Max retries exceeded")) + } + + private suspend fun throttleRequest() { + rateLimitMutex.withLock { + val now = System.currentTimeMillis() + val elapsed = now - lastRequestTimeMs + if (elapsed < MIN_REQUEST_INTERVAL_MS) { + delay(MIN_REQUEST_INTERVAL_MS - elapsed) + } + lastRequestTimeMs = System.currentTimeMillis() + } + } + + private fun parseHttpError(code: Int, response: String): CardanoException { + return when (code) { + 429 -> CardanoException.RateLimitException() + 404 -> CardanoException.ApiException("Resource not found", response) + in 500..599 -> CardanoException.NetworkException("Server error", statusCode = code) + else -> CardanoException.ApiException("HTTP $code: $response", response) + } + } + + private fun parseError(response: String?): CardanoException { + if (response == null) { + return CardanoException.NetworkException("No response from server") + } + + return when { + response.contains("429") -> CardanoException.RateLimitException() + response.contains("404") -> CardanoException.ApiException("Resource not found", response) + response.contains("500") || response.contains("502") || response.contains("503") -> { + CardanoException.NetworkException("Server error", statusCode = 500) + } + else -> CardanoException.ApiException("API error: $response", response) + } + } + + private fun String.hexToByteArray(): ByteArray { + require(length % 2 == 0) { "Hex string must have even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/PaymentStatusPoller.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/PaymentStatusPoller.kt new file mode 100644 index 0000000000..988b074a5e --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/cardano/PaymentStatusPoller.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.cardano + +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.di.SessionScope +import dev.zacsweers.metro.SingleIn +import io.element.android.features.wallet.api.CardanoClient +import io.element.android.features.wallet.api.PaymentStatusPoller +import io.element.android.features.wallet.api.TxStatus +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import timber.log.Timber +import dev.zacsweers.metro.Inject + +/** + * Default implementation of [PaymentStatusPoller]. + */ +@SingleIn(SessionScope::class) +@ContributesBinding(SessionScope::class) +class DefaultPaymentStatusPoller @Inject constructor( + private val cardanoClient: CardanoClient, +) : PaymentStatusPoller { + + companion object { + private const val TAG = "PaymentStatusPoller" + + /** Interval between polls in milliseconds */ + private const val POLL_INTERVAL_MS = 10_000L // 10 seconds + + /** Maximum number of polling attempts */ + private const val MAX_ATTEMPTS = 60 // ~10 minutes total + + /** Initial delay before first poll (give network time to propagate) */ + private const val INITIAL_DELAY_MS = 5_000L // 5 seconds + } + + override fun pollUntilConfirmed(txHash: String): Flow = flow { + Timber.tag(TAG).d("Starting to poll for tx: $txHash") + + // Emit initial PENDING status + emit(TxStatus.PENDING) + + // Wait a bit before first poll (transaction needs time to propagate) + delay(INITIAL_DELAY_MS) + + var attempts = 0 + var lastStatus = TxStatus.PENDING + + while (attempts < MAX_ATTEMPTS && lastStatus == TxStatus.PENDING) { + attempts++ + Timber.tag(TAG).d("Poll attempt $attempts/$MAX_ATTEMPTS for tx: $txHash") + + try { + val result = cardanoClient.getTxStatus(txHash) + + result.fold( + onSuccess = { status -> + if (status != lastStatus) { + Timber.tag(TAG).i("Tx $txHash status changed: $lastStatus -> $status") + lastStatus = status + emit(status) + } + }, + onFailure = { error -> + Timber.tag(TAG).w(error, "Error polling tx $txHash (attempt $attempts)") + // Don't emit FAILED on transient errors, continue polling + } + ) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Exception polling tx $txHash") + // Continue polling on error + } + + // Don't wait if we're done polling + if (lastStatus == TxStatus.PENDING && attempts < MAX_ATTEMPTS) { + delay(POLL_INTERVAL_MS) + } + } + + // If we exhausted attempts without confirmation, mark as potentially failed + if (lastStatus == TxStatus.PENDING) { + Timber.tag(TAG).w("Tx $txHash not confirmed after $MAX_ATTEMPTS attempts") + // Note: Transaction might still confirm later, but we stop polling + // The status remains PENDING, not FAILED, because the tx might still be valid + } + + Timber.tag(TAG).d("Stopped polling for tx: $txHash (final status: $lastStatus)") + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/di/WalletModule.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/di/WalletModule.kt new file mode 100644 index 0000000000..fa0f5b3f29 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/di/WalletModule.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.di + +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.AppScope +import kotlinx.serialization.json.Json + +/** + * DI module providing wallet-related dependencies. + * + * Note: CardanoClient binding is handled via @ContributesBinding + * annotation on KoiosCardanoClient. + */ +@ContributesTo(AppScope::class) +@BindingContainer +interface WalletModule { + companion object { + @Provides + @SingleIn(AppScope::class) + fun provideWalletJson(): Json = Json { + ignoreUnknownKeys = true + isLenient = true + encodeDefaults = true + } + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletDeleteConfirmationDialog.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletDeleteConfirmationDialog.kt new file mode 100644 index 0000000000..ff86333fac --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletDeleteConfirmationDialog.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.panel + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +/** + * A non-dismissible confirmation dialog for wallet deletion with a clear warning. + */ +@Composable +fun WalletDeleteConfirmationDialog( + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + // Block back button - must explicitly choose Cancel or Delete + BackHandler(enabled = true) { + // Intentionally empty - prevent back press from dismissing + } + + AlertDialog( + onDismissRequest = { + // Cannot dismiss by tapping outside - must choose an action + }, + icon = { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.error, + ) + }, + title = { + Text( + text = "Delete Wallet?", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + }, + text = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + ) { + Text( + text = "This will permanently remove your wallet from this device. If you haven't backed up your recovery phrase, " + + "you will lose access to your funds forever.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Make sure you have:", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "• Written down your 24-word recovery phrase, OR\n• Backed up to Matrix", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + confirmButton = { + TextButton( + onClick = onConfirm, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Text( + text = "Delete Wallet", + fontWeight = FontWeight.Bold, + ) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + }, + ) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelNode.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelNode.kt new file mode 100644 index 0000000000..e8db906a81 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelNode.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.panel + +import android.content.Intent +import android.net.Uri +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.features.wallet.impl.cardano.CardanoNetworkConfig +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.di.SessionScope + +/** + * Node for displaying the wallet panel. + */ +@ContributesNode(SessionScope::class) +class WalletPanelNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: WalletPanelPresenter, +) : Node( + buildContext = buildContext, + plugins = plugins, +) { + /** + * Callback interface for wallet panel navigation events. + */ + interface Callback : Plugin { + fun onClose() + fun onSendAda() + fun onSetupWallet() + } + + private val callback: Callback = callback() + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current + + WalletPanelView( + state = state.copy( + eventSink = { event -> + when (event) { + is WalletPanelEvent.OpenTransaction -> { + val url = "${CardanoNetworkConfig.EXPLORER_BASE_URL}/transaction/${event.txHash}" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) + } + else -> state.eventSink(event) + } + } + ), + onBackClick = { callback.onClose() }, + onSendClick = { callback.onSendAda() }, + onSetupClick = { callback.onSetupWallet() }, + modifier = modifier, + ) + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelPresenter.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelPresenter.kt new file mode 100644 index 0000000000..6c9da498a2 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelPresenter.kt @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.panel + +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject +import io.element.android.features.wallet.api.CardanoClient +import io.element.android.features.wallet.api.NativeAsset +import io.element.android.features.wallet.api.NftMetadata +import io.element.android.features.wallet.api.TxSummary +import io.element.android.features.wallet.api.backup.WalletBackupService +import io.element.android.features.wallet.api.storage.CardanoKeyStorage +import io.element.android.features.wallet.impl.cardano.CardanoNetworkConfig +import io.element.android.features.wallet.impl.cardano.CardanoWalletManager +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +/** + * Presenter for the wallet panel. + */ +class WalletPanelPresenter @Inject constructor( + private val walletManager: CardanoWalletManager, + private val cardanoClient: CardanoClient, + private val matrixClient: MatrixClient, + private val walletBackupService: WalletBackupService, + private val keyStorage: CardanoKeyStorage, +) : Presenter { + + @Composable + override fun present(): WalletPanelState { + val walletState by walletManager.walletState.collectAsState() + val scope = rememberCoroutineScope() + + var assets by remember { mutableStateOf>(emptyList()) } + var transactions by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(true) } + var error by remember { mutableStateOf(null) } + + // Mnemonic dialog state + var requestBiometricAuth by remember { mutableStateOf(false) } + var showMnemonicDialog by remember { mutableStateOf(false) } + var mnemonicWords by remember { mutableStateOf?>(null) } + var mnemonicError by remember { mutableStateOf(null) } + + // SSSS Backup state + var showBackupDialog by remember { mutableStateOf(false) } + var backupMode by remember { mutableStateOf(BackupMode.BACKUP) } + var backupInProgress by remember { mutableStateOf(false) } + var backupError by remember { mutableStateOf(null) } + var backupSuccess by remember { mutableStateOf(null) } + + // Delete confirmation state + var showDeleteConfirmation by remember { mutableStateOf(false) } + + // Initialize wallet on first composition + LaunchedEffect(Unit) { + walletManager.initialize(matrixClient.sessionId) + } + + // Load assets and transactions when we have an address + LaunchedEffect(walletState.address) { + val address = walletState.address ?: run { isLoading = false; return@LaunchedEffect } + + isLoading = true + error = null + + try { + // Fetch balance + val balanceResult = cardanoClient.getBalance(address) + balanceResult.onSuccess { balance -> + walletManager.refreshBalance(matrixClient.sessionId, balance) + } + + // Fetch assets and enrich with NFT metadata + cardanoClient.getAddressAssets(address) + .onSuccess { fetchedAssets -> + assets = enrichAssetsWithMetadata(fetchedAssets) + } + .onFailure { Timber.w(it, "Failed to fetch assets") } + + // Fetch transactions + cardanoClient.getAddressTransactions(address, 20) + .onSuccess { transactions = it } + .onFailure { Timber.w(it, "Failed to fetch transactions") } + } catch (e: Exception) { + Timber.e(e, "Failed to load wallet data") + error = e.message + } finally { + isLoading = false + } + } + + fun handleEvent(event: WalletPanelEvent) { + when (event) { + WalletPanelEvent.Refresh -> { + // Trigger refresh - handled by LaunchedEffect + } + WalletPanelEvent.ExportRecoveryPhrase -> { + // Signal the view to trigger biometric auth + requestBiometricAuth = true + } + WalletPanelEvent.CancelBiometricAuth -> { + requestBiometricAuth = false + } + WalletPanelEvent.LoadMnemonic -> { + requestBiometricAuth = false + scope.launch { + mnemonicError = null + walletManager.getMnemonic(matrixClient.sessionId) + .onSuccess { words -> + mnemonicWords = words + showMnemonicDialog = true + } + .onFailure { e -> + Timber.e(e, "Failed to get mnemonic") + mnemonicError = e.message ?: "Failed to retrieve recovery phrase" + } + } + } + WalletPanelEvent.DismissMnemonicDialog -> { + showMnemonicDialog = false + mnemonicWords = null + mnemonicError = null + } + WalletPanelEvent.DeleteWallet -> { + // Show confirmation dialog + showDeleteConfirmation = true + } + WalletPanelEvent.ConfirmDeleteWallet -> { + scope.launch { + Timber.i("Deleting wallet for session ${matrixClient.sessionId}") + keyStorage.deleteWallet(matrixClient.sessionId) + .onSuccess { + Timber.i("Wallet deleted successfully") + showDeleteConfirmation = false + // Reset wallet state - this will cause the panel to show setup prompt + walletManager.clearState() + } + .onFailure { e -> + Timber.e(e, "Failed to delete wallet") + error = e.message ?: "Failed to delete wallet" + showDeleteConfirmation = false + } + } + } + WalletPanelEvent.CancelDeleteWallet -> { + showDeleteConfirmation = false + } + is WalletPanelEvent.OpenTransaction -> { + // Handled by view via intent + } + WalletPanelEvent.Close -> { + // Navigation handled by node callback + } + // SSSS Backup events + WalletPanelEvent.ShowBackupDialog -> { + backupMode = BackupMode.BACKUP + backupError = null + backupSuccess = null + showBackupDialog = true + } + WalletPanelEvent.ShowRestoreDialog -> { + backupMode = BackupMode.RESTORE + backupError = null + backupSuccess = null + showBackupDialog = true + } + WalletPanelEvent.DismissBackupDialog -> { + showBackupDialog = false + backupError = null + } + is WalletPanelEvent.ConfirmBackup -> { + scope.launch { + backupInProgress = true + backupError = null + + // Normalize recovery key: remove spaces and convert to lowercase + val normalizedKey = event.recoveryKey.replace("\\s+".toRegex(), "").lowercase() + + walletManager.getMnemonic(matrixClient.sessionId) + .onSuccess { mnemonic -> + walletBackupService.backupSeed(normalizedKey, mnemonic) + .onSuccess { + Timber.i("Wallet backed up to SSSS successfully") + backupSuccess = "Wallet backed up successfully" + showBackupDialog = false + } + .onFailure { e -> + Timber.e(e, "Failed to backup wallet to SSSS") + backupError = e.message ?: "Failed to backup wallet" + } + } + .onFailure { e -> + Timber.e(e, "Failed to get mnemonic for backup") + backupError = e.message ?: "Failed to retrieve wallet data" + } + + backupInProgress = false + } + } + is WalletPanelEvent.ConfirmRestore -> { + scope.launch { + backupInProgress = true + backupError = null + + // Normalize recovery key: remove spaces and convert to lowercase + val normalizedKey = event.recoveryKey.replace("\\s+".toRegex(), "").lowercase() + + walletBackupService.restoreSeed(normalizedKey) + .onSuccess { mnemonic -> + if (mnemonic != null) { + // First delete existing wallet if any + keyStorage.deleteWallet(matrixClient.sessionId) + + // Import the restored mnemonic + keyStorage.importWallet(matrixClient.sessionId, mnemonic) + .onSuccess { + Timber.i("Wallet restored from SSSS successfully") + backupSuccess = "Wallet restored successfully" + showBackupDialog = false + // Reinitialize wallet state + walletManager.initialize(matrixClient.sessionId) + } + .onFailure { e -> + Timber.e(e, "Failed to import restored wallet") + backupError = e.message ?: "Failed to import wallet" + } + } else { + backupError = "No wallet backup found in Matrix" + } + } + .onFailure { e -> + Timber.e(e, "Failed to restore wallet from SSSS") + backupError = e.message ?: "Failed to restore wallet" + } + + backupInProgress = false + } + } + WalletPanelEvent.ClearBackupMessage -> { + backupError = null + backupSuccess = null + } + else -> { + // Other events handled elsewhere + } + } + } + + return WalletPanelState( + hasWallet = walletState.hasWallet, + isLoading = isLoading || walletState.isLoading, + address = walletState.address, + balanceLovelace = walletState.balanceLovelace, + balanceAda = walletState.balanceAda, + assets = assets, + transactions = transactions, + isTestnet = CardanoNetworkConfig.NETWORK_NAME != "mainnet", + error = error ?: walletState.error, + requestBiometricAuth = requestBiometricAuth, + showMnemonicDialog = showMnemonicDialog, + mnemonicWords = mnemonicWords, + mnemonicError = mnemonicError, + showBackupDialog = showBackupDialog, + backupMode = backupMode, + backupInProgress = backupInProgress, + backupError = backupError, + backupSuccess = backupSuccess, + showDeleteConfirmation = showDeleteConfirmation, + eventSink = ::handleEvent, + ) + } + + /** + * Enrich assets with NFT metadata from Koios. + * Fetches CIP-25 metadata for potential NFTs (quantity == 1) in parallel. + */ + private suspend fun enrichAssetsWithMetadata(assets: List): List { + if (assets.isEmpty()) return assets + + // Identify potential NFTs (quantity == 1 or marked as NFT) + val potentialNfts = assets.filter { it.quantity == 1L || it.isNft } + if (potentialNfts.isEmpty()) return assets + + Timber.d("Enriching ${potentialNfts.size} potential NFTs with metadata") + + // Fetch metadata in parallel (max 10 concurrent to avoid rate limiting) + val metadataMap = mutableMapOf() + try { + coroutineScope { + potentialNfts.chunked(10).forEach { chunk -> + chunk.map { asset -> + async { + cardanoClient.getNftMetadata(asset.policyId, asset.assetName) + .onSuccess { metadata -> + if (metadata != null) { + metadataMap[asset.unit] = metadata + } + } + .onFailure { e -> + Timber.w(e, "Failed to fetch metadata for ${asset.unit}") + } + } + }.awaitAll() + } + } + } catch (e: Exception) { + Timber.w(e, "Error during metadata enrichment, continuing without full metadata") + } + + Timber.d("Successfully fetched metadata for ${metadataMap.size} NFTs") + + // Apply metadata to assets + return assets.map { asset -> + val metadata = metadataMap[asset.unit] + if (metadata != null) { + asset.copy( + displayName = metadata.name.takeIf { it.isNotEmpty() } ?: asset.displayName, + imageUrl = metadata.image, + description = metadata.description, + isNft = metadata.image != null, + ) + } else { + asset + } + } + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelState.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelState.kt new file mode 100644 index 0000000000..2d44391675 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelState.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.panel + +import androidx.compose.runtime.Immutable +import io.element.android.features.wallet.api.NativeAsset +import io.element.android.features.wallet.api.TxSummary + +/** + * UI state for the wallet panel. + */ +@Immutable +data class WalletPanelState( + val hasWallet: Boolean, + val isLoading: Boolean, + val address: String?, + val balanceLovelace: Long?, + val balanceAda: String?, + val assets: List, + val transactions: List, + val isTestnet: Boolean, + val error: String?, + val requestBiometricAuth: Boolean, + val showMnemonicDialog: Boolean, + val mnemonicWords: List?, + val mnemonicError: String?, + // SSSS Backup state + val showBackupDialog: Boolean, + val backupMode: BackupMode, + val backupInProgress: Boolean, + val backupError: String?, + val backupSuccess: String?, + // Delete confirmation state + val showDeleteConfirmation: Boolean, + val eventSink: (WalletPanelEvent) -> Unit, +) { + companion object { + val Initial = WalletPanelState( + hasWallet = false, + isLoading = true, + address = null, + balanceLovelace = null, + balanceAda = null, + assets = emptyList(), + transactions = emptyList(), + isTestnet = true, + error = null, + requestBiometricAuth = false, + showMnemonicDialog = false, + mnemonicWords = null, + mnemonicError = null, + showBackupDialog = false, + backupMode = BackupMode.BACKUP, + backupInProgress = false, + backupError = null, + backupSuccess = null, + showDeleteConfirmation = false, + eventSink = {}, + ) + } + + /** + * Truncated address for display (first 12 + last 8 chars). + */ + val truncatedAddress: String? + get() = address?.let { addr -> + if (addr.length > 24) { + "${addr.take(12)}...${addr.takeLast(8)}" + } else { + addr + } + } +} + +/** + * Backup operation mode. + */ +enum class BackupMode { + BACKUP, + RESTORE +} + +/** + * Events that can be triggered from the wallet panel UI. + */ +sealed interface WalletPanelEvent { + /** Refresh wallet data from the network. */ + data object Refresh : WalletPanelEvent + + /** Navigate to send ADA flow. */ + data object SendAda : WalletPanelEvent + + /** Copy address to clipboard. */ + data object CopyAddress : WalletPanelEvent + + /** Navigate to wallet setup flow. */ + data object SetupWallet : WalletPanelEvent + + /** Export recovery phrase (triggers biometric auth). */ + data object ExportRecoveryPhrase : WalletPanelEvent + + /** Called after successful biometric auth to load mnemonic. */ + data object LoadMnemonic : WalletPanelEvent + + /** Cancel the biometric auth request. */ + data object CancelBiometricAuth : WalletPanelEvent + + /** Dismiss the mnemonic dialog. */ + data object DismissMnemonicDialog : WalletPanelEvent + + /** Show delete confirmation dialog. */ + data object DeleteWallet : WalletPanelEvent + + /** Confirm wallet deletion. */ + data object ConfirmDeleteWallet : WalletPanelEvent + + /** Cancel wallet deletion / dismiss dialog. */ + data object CancelDeleteWallet : WalletPanelEvent + + /** Open transaction in block explorer. */ + data class OpenTransaction(val txHash: String) : WalletPanelEvent + + /** Close the panel. */ + data object Close : WalletPanelEvent + + // SSSS Backup events + /** Show backup dialog to enter recovery key. */ + data object ShowBackupDialog : WalletPanelEvent + + /** Show restore dialog to enter recovery key. */ + data object ShowRestoreDialog : WalletPanelEvent + + /** Dismiss the backup/restore dialog. */ + data object DismissBackupDialog : WalletPanelEvent + + /** Confirm backup with the provided recovery key. */ + data class ConfirmBackup(val recoveryKey: String) : WalletPanelEvent + + /** Confirm restore with the provided recovery key. */ + data class ConfirmRestore(val recoveryKey: String) : WalletPanelEvent + + /** Clear backup success/error message. */ + data object ClearBackupMessage : WalletPanelEvent +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelView.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelView.kt new file mode 100644 index 0000000000..7dde957b20 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/WalletPanelView.kt @@ -0,0 +1,511 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.panel + +import android.view.WindowManager +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogProperties +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.wallet.impl.R +import io.element.android.features.wallet.impl.panel.tabs.AssetsTabView +import io.element.android.features.wallet.impl.panel.tabs.HistoryTabView +import io.element.android.features.wallet.impl.panel.tabs.OverviewTabView +import io.element.android.features.wallet.impl.panel.tabs.SettingsTabView +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.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import kotlinx.coroutines.launch +import timber.log.Timber + +private enum class WalletTab(val titleRes: Int) { + Overview(R.string.wallet_tab_overview), + Assets(R.string.wallet_tab_assets), + History(R.string.wallet_tab_history), + Settings(R.string.wallet_tab_settings), +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WalletPanelView( + state: WalletPanelState, + onBackClick: () -> Unit, + onSendClick: () -> Unit, + onSetupClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val tabs = WalletTab.entries + val pagerState = rememberPagerState(pageCount = { tabs.size }) + val scope = rememberCoroutineScope() + val context = LocalContext.current + val activity = context as? FragmentActivity + + // Handle biometric authentication request + LaunchedEffect(state.requestBiometricAuth) { + if (state.requestBiometricAuth && activity != null) { + val biometricManager = BiometricManager.from(context) + val canAuth = biometricManager.canAuthenticate( + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) == BiometricManager.BIOMETRIC_SUCCESS + + if (canAuth) { + val executor = ContextCompat.getMainExecutor(context) + val callback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + state.eventSink(WalletPanelEvent.LoadMnemonic) + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + Timber.w("Biometric auth error: $errorCode - $errString") + state.eventSink(WalletPanelEvent.CancelBiometricAuth) + } + + override fun onAuthenticationFailed() { + // User can retry + } + } + + val biometricPrompt = BiometricPrompt(activity, executor, callback) + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle("Confirm your identity") + .setSubtitle("Authenticate to view recovery phrase") + .setAllowedAuthenticators( + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + .build() + + biometricPrompt.authenticate(promptInfo) + } else { + // No biometric/credential available, proceed directly + state.eventSink(WalletPanelEvent.LoadMnemonic) + } + } + } + + // Show mnemonic dialog + if (state.showMnemonicDialog && state.mnemonicWords != null) { + MnemonicDisplayDialog( + words = state.mnemonicWords, + onDismiss = { state.eventSink(WalletPanelEvent.DismissMnemonicDialog) } + ) + } + + // Show backup/restore dialog + if (state.showBackupDialog) { + BackupRecoveryKeyDialog( + mode = state.backupMode, + isLoading = state.backupInProgress, + error = state.backupError, + onConfirm = { recoveryKey -> + when (state.backupMode) { + BackupMode.BACKUP -> state.eventSink(WalletPanelEvent.ConfirmBackup(recoveryKey)) + BackupMode.RESTORE -> state.eventSink(WalletPanelEvent.ConfirmRestore(recoveryKey)) + } + }, + onDismiss = { state.eventSink(WalletPanelEvent.DismissBackupDialog) } + ) + } + + // Show delete confirmation dialog + if (state.showDeleteConfirmation) { + WalletDeleteConfirmationDialog( + onConfirm = { state.eventSink(WalletPanelEvent.ConfirmDeleteWallet) }, + onDismiss = { state.eventSink(WalletPanelEvent.CancelDeleteWallet) } + ) + } + + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.wallet_panel_title)) }, + navigationIcon = { + BackButton(onClick = onBackClick) + }, + ) + }, + ) { padding -> + if (!state.hasWallet && !state.isLoading) { + // Show setup prompt + WalletSetupPromptView( + onSetupClick = onSetupClick, + modifier = Modifier + .fillMaxSize() + .padding(padding), + ) + } else { + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding), + ) { + TabRow( + selectedTabIndex = pagerState.currentPage, + ) { + tabs.forEachIndexed { index, tab -> + Tab( + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = { Text(stringResource(tab.titleRes)) }, + ) + } + } + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + ) { page -> + when (tabs[page]) { + WalletTab.Overview -> OverviewTabView( + state = state, + onSendClick = onSendClick, + modifier = Modifier.fillMaxSize(), + ) + WalletTab.Assets -> AssetsTabView( + assets = state.assets, + isLoading = state.isLoading, + modifier = Modifier.fillMaxSize(), + ) + WalletTab.History -> HistoryTabView( + transactions = state.transactions, + isTestnet = state.isTestnet, + isLoading = state.isLoading, + onTransactionClick = { txHash -> + state.eventSink(WalletPanelEvent.OpenTransaction(txHash)) + }, + modifier = Modifier.fillMaxSize(), + ) + WalletTab.Settings -> SettingsTabView( + address = state.address, + isTestnet = state.isTestnet, + onCopyAddress = { state.eventSink(WalletPanelEvent.CopyAddress) }, + onExportPhrase = { state.eventSink(WalletPanelEvent.ExportRecoveryPhrase) }, + onBackupToMatrix = { state.eventSink(WalletPanelEvent.ShowBackupDialog) }, + onDeleteWallet = { state.eventSink(WalletPanelEvent.DeleteWallet) }, + modifier = Modifier.fillMaxSize(), + ) + } + } + } + } + } +} + +@Composable +private fun BackupRecoveryKeyDialog( + mode: BackupMode, + isLoading: Boolean, + error: String?, + onConfirm: (String) -> Unit, + onDismiss: () -> Unit, +) { + var recoveryKey by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = { if (!isLoading) onDismiss() }, + properties = DialogProperties( + dismissOnBackPress = !isLoading, + dismissOnClickOutside = !isLoading, + ), + title = { + Text( + text = stringResource(R.string.wallet_backup_dialog_title), + style = MaterialTheme.typography.headlineSmall, + ) + }, + text = { + Column { + Text( + text = stringResource(R.string.wallet_backup_dialog_message), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp), + ) + + OutlinedTextField( + value = recoveryKey, + onValueChange = { recoveryKey = it }, + label = { Text(stringResource(R.string.wallet_backup_dialog_hint)) }, + enabled = !isLoading, + singleLine = false, + minLines = 2, + maxLines = 4, + modifier = Modifier.fillMaxWidth(), + isError = error != null, + ) + + if (error != null) { + Text( + text = error, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(top = 8.dp), + ) + } + + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier + .padding(top = 16.dp) + .align(Alignment.CenterHorizontally), + ) + } + } + }, + confirmButton = { + Button( + onClick = { onConfirm(recoveryKey) }, + enabled = !isLoading && recoveryKey.isNotBlank(), + ) { + Text( + text = when (mode) { + BackupMode.BACKUP -> stringResource(R.string.wallet_backup_dialog_backup) + BackupMode.RESTORE -> stringResource(R.string.wallet_backup_dialog_restore) + } + ) + } + }, + dismissButton = { + TextButton( + onClick = onDismiss, + enabled = !isLoading, + ) { + Text(stringResource(R.string.wallet_backup_dialog_cancel)) + } + }, + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun MnemonicDisplayDialog( + words: List, + onDismiss: () -> Unit, +) { + val context = LocalContext.current + val activity = context as? android.app.Activity + + // Set FLAG_SECURE to prevent screenshots while dialog is shown + DisposableEffect(Unit) { + activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + onDispose { + activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + + AlertDialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = false, + usePlatformDefaultWidth = false, + ), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + title = { + Text( + text = "Recovery Phrase", + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + }, + text = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Write down these 24 words in order and store them safely. " + + "Never share your recovery phrase with anyone.", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 16.dp), + ) + + // 4 columns x 6 rows grid + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + verticalArrangement = Arrangement.spacedBy(8.dp), + maxItemsInEachRow = 4, + ) { + words.forEachIndexed { index, word -> + WordChip( + number = index + 1, + word = word, + ) + } + } + } + }, + confirmButton = { + Button( + onClick = onDismiss, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Done") + } + }, + ) +} + +@Composable +private fun WordChip( + number: Int, + word: String, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(width = 80.dp, height = 36.dp) + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp), + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = "$number. $word", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + maxLines = 1, + ) + } +} + +@Composable +private fun WalletSetupPromptView( + onSetupClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + androidx.compose.material3.Icon( + imageVector = CompoundIcons.Chart(), + contentDescription = null, + modifier = Modifier + .padding(bottom = 16.dp) + .then(Modifier.padding(48.dp)), + ) + Text( + text = stringResource(R.string.wallet_setup_title), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 8.dp), + ) + Text( + text = stringResource(R.string.wallet_setup_description), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 24.dp), + ) + Button(onClick = onSetupClick) { + Text(stringResource(R.string.wallet_setup_button)) + } + } +} + +@PreviewsDayNight +@Composable +internal fun WalletPanelViewPreview() = ElementPreview { + WalletPanelView( + state = WalletPanelState( + hasWallet = true, + isLoading = false, + address = "addr_test1qpu5vlrf4xkxs2m4wcn7hpq98aqspflj3tdx8ax9qk9qw8zqh2c4tkqehp4j0y8awxmjcgv5p2vz8z5zycq7vq4q2dqst7pf8y", + balanceLovelace = 5_500_000L, + balanceAda = "5.5", + assets = emptyList(), + transactions = emptyList(), + isTestnet = true, + error = null, + requestBiometricAuth = false, + showMnemonicDialog = false, + mnemonicWords = null, + mnemonicError = null, + showBackupDialog = false, + backupMode = BackupMode.BACKUP, + backupInProgress = false, + backupError = null, + backupSuccess = null, + showDeleteConfirmation = false, + eventSink = {}, + ), + onBackClick = {}, + onSendClick = {}, + onSetupClick = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun WalletPanelViewNoWalletPreview() = ElementPreview { + WalletPanelView( + state = WalletPanelState.Initial.copy( + hasWallet = false, + isLoading = false, + ), + onBackClick = {}, + onSendClick = {}, + onSetupClick = {}, + ) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/AssetsTabView.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/AssetsTabView.kt new file mode 100644 index 0000000000..2c76af340e --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/AssetsTabView.kt @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.panel.tabs + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.compose.AsyncImagePainter +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.wallet.api.NativeAsset +import io.element.android.features.wallet.impl.R +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 + +@Composable +fun AssetsTabView( + assets: List, + isLoading: Boolean, + modifier: Modifier = Modifier, +) { + var selectedNft by remember { mutableStateOf(null) } + + Box(modifier = modifier) { + when { + isLoading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + ) + } + assets.isEmpty() -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = CompoundIcons.Files(), + contentDescription = null, + modifier = Modifier.padding(bottom = 16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = stringResource(R.string.wallet_no_assets), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(assets) { asset -> + AssetCard( + asset = asset, + onClick = { if (asset.isNft || asset.imageUrl != null) selectedNft = asset }, + ) + } + } + } + } + } + + // NFT Detail Bottom Sheet + selectedNft?.let { nft -> + NftDetailBottomSheet( + asset = nft, + onDismiss = { selectedNft = null }, + ) + } +} + +@Composable +private fun AssetCard( + asset: NativeAsset, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hasImage = asset.imageUrl != null + + Card( + modifier = modifier + .fillMaxWidth() + .then( + if (hasImage) { + Modifier.clickable(onClick = onClick) + } else { + Modifier + } + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // NFT Thumbnail (64dp square, 8dp rounded corners) + if (hasImage) { + NftThumbnail( + imageUrl = asset.imageUrl!!, + contentDescription = asset.name, + modifier = Modifier.size(64.dp), + ) + } else { + // Placeholder for non-NFT tokens + Box( + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = CompoundIcons.Info(), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + } + } + + // Asset info + Column( + modifier = Modifier.weight(1f), + ) { + Text( + text = asset.name, + style = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = asset.truncatedPolicyId, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (asset.isNft) { + Text( + text = "NFT", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + ) + } + } + + // Quantity + Text( + text = asset.formatQuantity(), + style = MaterialTheme.typography.titleMedium, + ) + } + } +} + +@Composable +private fun NftThumbnail( + imageUrl: String, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + var isLoading by remember { mutableStateOf(true) } + var isError by remember { mutableStateOf(false) } + + Box( + modifier = modifier + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + AsyncImage( + model = imageUrl, + contentDescription = contentDescription, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + onState = { state -> + isLoading = state is AsyncImagePainter.State.Loading + isError = state is AsyncImagePainter.State.Error + }, + ) + + // Loading indicator + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp, + ) + } + + // Error placeholder + if (isError) { + Icon( + imageVector = CompoundIcons.Error(), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun NftDetailBottomSheet( + asset: NativeAsset, + onDismiss: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // Large NFT image + asset.imageUrl?.let { url -> + NftDetailImage( + imageUrl = url, + contentDescription = asset.name, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .padding(bottom = 16.dp), + ) + } + + // NFT Name + Text( + text = asset.name, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 8.dp), + ) + + // Policy ID + Text( + text = asset.truncatedPolicyId, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 16.dp), + ) + + // Description if available + asset.description?.let { description -> + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 16.dp), + ) + } + + // Quantity badge + Row( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Quantity: ${asset.formatQuantity()}", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + } + } +} + +@Composable +private fun NftDetailImage( + imageUrl: String, + contentDescription: String?, + modifier: Modifier = Modifier, +) { + var isLoading by remember { mutableStateOf(true) } + var isError by remember { mutableStateOf(false) } + + Box( + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + AsyncImage( + model = imageUrl, + contentDescription = contentDescription, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit, + onState = { state -> + isLoading = state is AsyncImagePainter.State.Loading + isError = state is AsyncImagePainter.State.Error + }, + ) + + // Loading indicator + if (isLoading) { + CircularProgressIndicator() + } + + // Error placeholder + if (isError) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = CompoundIcons.Error(), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(48.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Failed to load image", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun AssetsTabViewPreview() = ElementPreview { + AssetsTabView( + assets = listOf( + NativeAsset( + policyId = "aabbccdd11223344556677889900aabbccdd11223344556677889900", + assetName = "4d79546f6b656e", + quantity = 1000, + displayName = "MyToken", + fingerprint = null, + ), + NativeAsset( + policyId = "11223344556677889900aabbccdd11223344556677889900aabbccdd", + assetName = "436f6f6c4e4654", + quantity = 1, + displayName = "CoolNFT", + fingerprint = null, + imageUrl = "https://ipfs.io/ipfs/QmTest123", + isNft = true, + description = "A really cool NFT from the Cardano blockchain", + ), + ), + isLoading = false, + ) +} + +@PreviewsDayNight +@Composable +internal fun AssetsTabViewEmptyPreview() = ElementPreview { + AssetsTabView( + assets = emptyList(), + isLoading = false, + ) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/HistoryTabView.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/HistoryTabView.kt new file mode 100644 index 0000000000..30f3c025db --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/HistoryTabView.kt @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.panel.tabs + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.wallet.api.TxSummary +import io.element.android.features.wallet.impl.R +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 + +@Composable +fun HistoryTabView( + transactions: List, + isTestnet: Boolean, + isLoading: Boolean, + onTransactionClick: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + when { + isLoading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + ) + } + transactions.isEmpty() -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = CompoundIcons.History(), + contentDescription = null, + modifier = Modifier.padding(bottom = 16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = stringResource(R.string.wallet_no_transactions), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(transactions) { tx -> + TransactionCard( + transaction = tx, + isTestnet = isTestnet, + onClick = { onTransactionClick(tx.txHash) }, + ) + } + } + } + } + } +} + +@Composable +private fun TransactionCard( + transaction: TxSummary, + isTestnet: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = when (transaction.direction) { + TxSummary.Direction.SENT -> CompoundIcons.ArrowUpRight() + TxSummary.Direction.RECEIVED -> CompoundIcons.ArrowDown() + }, + contentDescription = null, + tint = when (transaction.direction) { + TxSummary.Direction.SENT -> MaterialTheme.colorScheme.error + TxSummary.Direction.RECEIVED -> MaterialTheme.colorScheme.primary + }, + modifier = Modifier.padding(end = 8.dp), + ) + Text( + text = when (transaction.direction) { + TxSummary.Direction.SENT -> stringResource(R.string.wallet_tx_sent) + TxSummary.Direction.RECEIVED -> stringResource(R.string.wallet_tx_received) + }, + style = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.Medium, + ), + ) + } + Text( + text = transaction.formattedDate, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = transaction.truncatedTxHash, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Column( + horizontalAlignment = Alignment.End, + ) { + Text( + text = transaction.amountAda, + style = MaterialTheme.typography.titleMedium, + color = when (transaction.direction) { + TxSummary.Direction.SENT -> MaterialTheme.colorScheme.error + TxSummary.Direction.RECEIVED -> MaterialTheme.colorScheme.primary + }, + ) + Icon( + imageVector = CompoundIcons.PopOut(), + contentDescription = stringResource(R.string.wallet_view_on_explorer), + modifier = Modifier.padding(top = 4.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun HistoryTabViewPreview() = ElementPreview { + HistoryTabView( + transactions = listOf( + TxSummary( + txHash = "aabbccdd11223344556677889900aabbccdd11223344556677889900aabbccdd", + blockTime = 1710000000, + totalOutput = 5_500_000, + fee = 170000, + direction = TxSummary.Direction.SENT, + ), + TxSummary( + txHash = "11223344556677889900aabbccdd11223344556677889900aabbccdd11223344", + blockTime = 1709900000, + totalOutput = 10_000_000, + fee = 165000, + direction = TxSummary.Direction.RECEIVED, + ), + ), + isTestnet = true, + isLoading = false, + onTransactionClick = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun HistoryTabViewEmptyPreview() = ElementPreview { + HistoryTabView( + transactions = emptyList(), + isTestnet = true, + isLoading = false, + onTransactionClick = {}, + ) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/OverviewTabView.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/OverviewTabView.kt new file mode 100644 index 0000000000..5f2a7311d7 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/OverviewTabView.kt @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.panel.tabs + +import android.graphics.Bitmap +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.qrcode.QRCodeWriter +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.wallet.impl.R +import io.element.android.features.wallet.impl.panel.WalletPanelEvent +import io.element.android.features.wallet.impl.panel.BackupMode +import io.element.android.features.wallet.impl.panel.WalletPanelState +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 + +@Composable +fun OverviewTabView( + state: WalletPanelState, + onSendClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val clipboardManager = LocalClipboardManager.current + + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // Balance Card + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 24.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.wallet_balance_label), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(8.dp)) + + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(32.dp), + ) + } else { + Text( + text = "${state.balanceAda ?: "0"} ADA", + style = MaterialTheme.typography.displaySmall.copy( + fontWeight = FontWeight.Bold, + ), + ) + if (state.isTestnet) { + Text( + text = stringResource(R.string.wallet_testnet_label), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(top = 4.dp), + ) + } + } + } + } + + // QR Code + state.address?.let { address -> + val qrBitmap = remember(address) { + generateQrCode(address, 200) + } + qrBitmap?.let { bitmap -> + Box( + modifier = Modifier + .size(200.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color.White) + .padding(8.dp), + ) { + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = stringResource(R.string.wallet_qr_code_description), + modifier = Modifier.fillMaxSize(), + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Address + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { + clipboardManager.setText(AnnotatedString(address)) + state.eventSink(WalletPanelEvent.CopyAddress) + } + .padding(12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.truncatedAddress ?: address, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f, fill = false), + ) + Icon( + imageVector = CompoundIcons.Copy(), + contentDescription = stringResource(R.string.wallet_copy_address), + modifier = Modifier + .padding(start = 8.dp) + .size(20.dp), + ) + } + + Text( + text = stringResource(R.string.wallet_tap_to_copy), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + // Send Button + Button( + onClick = onSendClick, + modifier = Modifier.fillMaxWidth(), + enabled = state.hasWallet && !state.isLoading, + ) { + Icon( + imageVector = CompoundIcons.Send(), + contentDescription = null, + modifier = Modifier.padding(end = 8.dp), + ) + Text(stringResource(R.string.wallet_send_ada)) + } + } +} + +private fun generateQrCode(content: String, size: Int): Bitmap? { + return try { + val hints = mutableMapOf() + hints[EncodeHintType.MARGIN] = 0 + hints[EncodeHintType.CHARACTER_SET] = "UTF-8" + + val writer = QRCodeWriter() + val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, size, size, hints) + + val pixels = IntArray(size * size) + for (y in 0 until size) { + for (x in 0 until size) { + pixels[y * size + x] = if (bitMatrix[x, y]) { + android.graphics.Color.BLACK + } else { + android.graphics.Color.WHITE + } + } + } + + Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply { + setPixels(pixels, 0, size, 0, 0, size, size) + } + } catch (e: Exception) { + null + } +} + +@PreviewsDayNight +@Composable +internal fun OverviewTabViewPreview() = ElementPreview { + OverviewTabView( + state = WalletPanelState( + hasWallet = true, + isLoading = false, + address = "addr_test1qpu5vlrf4xkxs2m4wcn7hpq98aqspflj3tdx8ax9qk9qw8zqh2c4tkqehp4j0y8awxmjcgv5p2vz8z5zycq7vq4q2dqst7pf8y", + balanceLovelace = 25_500_000L, + balanceAda = "25.5", + assets = emptyList(), + transactions = emptyList(), + isTestnet = true, + error = null, + requestBiometricAuth = false, + showMnemonicDialog = false, + mnemonicWords = null, + mnemonicError = null, + showBackupDialog = false, + backupMode = BackupMode.BACKUP, + backupInProgress = false, + backupError = null, + backupSuccess = null, + showDeleteConfirmation = false, + eventSink = {}, + ), + onSendClick = {}, + ) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/SettingsTabView.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/SettingsTabView.kt new file mode 100644 index 0000000000..4b24bfbe93 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/panel/tabs/SettingsTabView.kt @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.panel.tabs + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.wallet.impl.R +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 + +@Composable +fun SettingsTabView( + address: String?, + isTestnet: Boolean, + onCopyAddress: () -> Unit, + onExportPhrase: () -> Unit, + onBackupToMatrix: () -> Unit, + onDeleteWallet: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + ) { + // Wallet Address Section + Card( + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier.padding(16.dp), + ) { + Text( + text = stringResource(R.string.wallet_settings_address), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = address ?: stringResource(R.string.wallet_settings_no_address), + style = MaterialTheme.typography.bodyMedium, + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onCopyAddress) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = CompoundIcons.Copy(), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(R.string.wallet_settings_copy_address), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 8.dp), + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Network Section + Card( + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(R.string.wallet_settings_network), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = if (isTestnet) { + stringResource(R.string.wallet_settings_testnet) + } else { + stringResource(R.string.wallet_settings_mainnet) + }, + style = MaterialTheme.typography.bodyLarge, + ) + } + if (isTestnet) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + ), + ) { + Text( + text = "TESTNET", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + ) + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Security Section + Card( + modifier = Modifier.fillMaxWidth(), + ) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onExportPhrase) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = CompoundIcons.Key(), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp), + ) { + Text( + text = stringResource(R.string.wallet_settings_export_phrase), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.wallet_settings_export_phrase_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon( + imageVector = CompoundIcons.ChevronRight(), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + HorizontalDivider() + + // Backup to Matrix + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onBackupToMatrix) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = CompoundIcons.Cloud(), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp), + ) { + Text( + text = stringResource(R.string.wallet_settings_backup_matrix), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.wallet_settings_backup_matrix_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon( + imageVector = CompoundIcons.ChevronRight(), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + HorizontalDivider() + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onDeleteWallet) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = CompoundIcons.Delete(), + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp), + ) { + Text( + text = stringResource(R.string.wallet_settings_delete_wallet), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + ) + Text( + text = stringResource(R.string.wallet_settings_delete_wallet_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun SettingsTabViewPreview() = ElementPreview { + SettingsTabView( + address = "addr_test1qpu5vlrf4xkxs2m4wcn7hpq98aqspflj3tdx8ax9qk9qw8zqh2c4tkqehp4j0y8awxmjcgv5p2vz8z5zycq7vq4q2dqst7pf8y", + isTestnet = true, + onCopyAddress = {}, + onExportPhrase = {}, + onBackupToMatrix = {}, + onDeleteWallet = {}, + ) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/DefaultPaymentEventSender.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/DefaultPaymentEventSender.kt new file mode 100644 index 0000000000..c618ee6682 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/DefaultPaymentEventSender.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.features.wallet.api.PaymentCardStatus +import io.element.android.features.wallet.api.PaymentEventSender +import io.element.android.features.wallet.api.PaymentRequest +import io.element.android.features.wallet.api.SignedTransaction +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.timeline.Timeline +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +/** + * Default implementation of [PaymentEventSender]. + * + * Sends Cardano payment events as custom Matrix event types via Timeline.sendRaw(). + * Events go through the send queue for reliability and encryption support. + */ +@ContributesBinding(SessionScope::class) +class DefaultPaymentEventSender @Inject constructor() : PaymentEventSender { + + private val json = Json { + encodeDefaults = true + ignoreUnknownKeys = true + } + + override suspend fun sendPaymentEvent( + timeline: Timeline, + request: PaymentRequest, + signedTx: SignedTransaction, + network: String, + ): Result { + val paymentData = PaymentEventData( + amountLovelace = signedTx.actualAmount, + toAddress = request.toAddress, + fromAddress = request.fromAddress, + txHash = signedTx.txHash, + status = PaymentCardStatus.PENDING.name.lowercase(), + network = network, + ) + + val jsonContent = json.encodeToString(paymentData) + + return timeline.sendRaw( + eventType = EVENT_TYPE_PAYMENT_REQUEST, + content = jsonContent, + ) + } + + override suspend fun sendStatusUpdate( + timeline: Timeline, + txHash: String, + newStatus: String, + network: String, + ): Result { + val statusData = PaymentStatusUpdateData( + txHash = txHash, + status = newStatus, + network = network, + ) + + val jsonContent = json.encodeToString(statusData) + + return timeline.sendRaw( + eventType = EVENT_TYPE_PAYMENT_STATUS, + content = jsonContent, + ) + } + + companion object { + /** Matrix event type for Cardano payment requests */ + const val EVENT_TYPE_PAYMENT_REQUEST = "co.sulkta.payment.request" + /** Matrix event type for payment status updates */ + const val EVENT_TYPE_PAYMENT_STATUS = "co.sulkta.payment.status" + + /** Legacy prefix for payment messages - kept for backward compatibility */ + const val PAYMENT_MESSAGE_PREFIX = "\$CARDANO_PAY$" + /** Legacy prefix for status update messages - kept for backward compatibility */ + const val STATUS_MESSAGE_PREFIX = "\$CARDANO_STATUS$" + } +} + +@kotlinx.serialization.Serializable +data class PaymentEventData( + val amountLovelace: Long, + val toAddress: String, + val fromAddress: String, + val txHash: String?, + val status: String, + val network: String, +) + +@kotlinx.serialization.Serializable +data class PaymentStatusUpdateData( + val txHash: String, + val status: String, + val network: String, +) diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationNode.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationNode.kt new file mode 100644 index 0000000000..8ce27b970a --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationNode.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.fragment.app.FragmentActivity +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.wallet.impl.biometric.BiometricAuthenticator +import io.element.android.features.wallet.impl.slash.Lovelace +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.di.SessionScope +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class PaymentConfirmationNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenterFactory: PaymentConfirmationPresenter.Factory, + private val biometricAuthenticator: BiometricAuthenticator, +) : Node(buildContext, plugins = plugins) { + + @Parcelize + data class Inputs( + val recipientAddress: String, + val amountLovelace: Lovelace, + val assetPolicyId: String?, + val assetName: String?, + val assetQuantity: Long?, + val assetDisplayName: String?, + ) : NodeInputs, Parcelable + + interface Callback : Plugin { + fun onConfirmed() + fun onBack() + } + + private val inputs: Inputs = plugins.filterIsInstance().first() + private val callback: Callback = plugins.filterIsInstance().first() + + private val presenter by lazy { + presenterFactory.create( + recipientAddress = inputs.recipientAddress, + amountLovelace = inputs.amountLovelace, + assetPolicyId = inputs.assetPolicyId, + assetName = inputs.assetName, + assetQuantity = inputs.assetQuantity, + assetDisplayName = inputs.assetDisplayName, + ) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + PaymentConfirmationView( + state = state, + onConfirm = { + coroutineScope.launch { + val activity = context as? FragmentActivity + if (activity == null) { + callback.onConfirmed() + return@launch + } + + // Build auth subtitle based on asset + val subtitle = if (state.isSendingToken && state.assetDisplayName != null) { + "Authenticate to send ${state.tokenQuantityDisplay} ${state.assetDisplayName}" + } else { + "Authenticate to send ${state.amountAda} ADA" + } + + val result = biometricAuthenticator.authenticate( + activity = activity, + title = "Confirm Payment", + subtitle = subtitle, + ) + + when (result) { + BiometricAuthenticator.AuthResult.Success -> { + callback.onConfirmed() + } + is BiometricAuthenticator.AuthResult.Error -> { + // Stay on screen + } + BiometricAuthenticator.AuthResult.Cancelled -> { + // Stay on screen + } + } + } + }, + onBack = { + callback.onBack() + }, + modifier = modifier, + ) + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationPresenter.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationPresenter.kt new file mode 100644 index 0000000000..e1f0abffca --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationPresenter.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import io.element.android.features.wallet.api.CardanoClient +import io.element.android.features.wallet.impl.cardano.CardanoNetwork +import io.element.android.features.wallet.impl.cardano.CardanoNetworkConfig +import io.element.android.features.wallet.impl.cardano.CardanoWalletManager +import io.element.android.features.wallet.impl.slash.Lovelace +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient + +/** + * Presenter for the payment confirmation screen. + */ +class PaymentConfirmationPresenter @AssistedInject constructor( + @Assisted private val recipientAddress: String, + @Assisted private val amountLovelace: Lovelace, + @Assisted private val assetPolicyId: String?, + @Assisted private val assetName: String?, + @Assisted private val assetQuantity: Long?, + @Assisted private val assetDisplayName: String?, + private val matrixClient: MatrixClient, + private val walletManager: CardanoWalletManager, + private val cardanoClient: CardanoClient, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create( + recipientAddress: String, + amountLovelace: Lovelace, + assetPolicyId: String?, + assetName: String?, + assetQuantity: Long?, + assetDisplayName: String?, + ): PaymentConfirmationPresenter + } + + companion object { + private const val ESTIMATED_TX_SIZE_BYTES = 350 + // Token transactions are larger + private const val ESTIMATED_TOKEN_TX_SIZE_BYTES = 450 + } + + @Composable + override fun present(): PaymentConfirmationState { + val sessionId = matrixClient.sessionId + val isSendingToken = assetPolicyId != null && assetQuantity != null + + var senderAddress by remember { mutableStateOf("") } + var senderBalanceLovelace by remember { mutableStateOf(null) } + var estimatedFeeLovelace by remember { mutableStateOf(null) } + var isFeeLoading by remember { mutableStateOf(true) } + var feeError by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + walletManager.getAddress(sessionId).onSuccess { address -> + senderAddress = address + } + + val address = walletManager.getAddress(sessionId).getOrNull() + if (address != null) { + cardanoClient.getBalance(address).onSuccess { balance -> + senderBalanceLovelace = balance + } + } + + cardanoClient.getProtocolParameters().onSuccess { params -> + val txSize = if (isSendingToken) ESTIMATED_TOKEN_TX_SIZE_BYTES else ESTIMATED_TX_SIZE_BYTES + val fee = params.minFeeA * txSize + params.minFeeB + estimatedFeeLovelace = fee + isFeeLoading = false + }.onFailure { + estimatedFeeLovelace = 200_000L + feeError = "Could not estimate exact fee" + isFeeLoading = false + } + } + + val totalLovelace = estimatedFeeLovelace?.let { amountLovelace + it } + + val insufficientFunds = senderBalanceLovelace != null && + totalLovelace != null && + senderBalanceLovelace!! < totalLovelace + + return PaymentConfirmationState( + recipientAddress = recipientAddress, + recipientAddressDisplay = PaymentConfirmationState.truncateAddress(recipientAddress), + amountLovelace = amountLovelace, + amountAda = PaymentConfirmationState.formatAda(amountLovelace), + estimatedFeeLovelace = estimatedFeeLovelace, + estimatedFeeAda = estimatedFeeLovelace?.let { PaymentConfirmationState.formatAda(it) }, + totalLovelace = totalLovelace, + totalAda = totalLovelace?.let { PaymentConfirmationState.formatAda(it) }, + senderAddress = senderAddress, + senderBalanceLovelace = senderBalanceLovelace, + insufficientFunds = insufficientFunds, + isTestnet = CardanoNetworkConfig.NETWORK == CardanoNetwork.TESTNET, + isFeeLoading = isFeeLoading, + feeError = feeError, + isSendingToken = isSendingToken, + assetPolicyId = assetPolicyId, + assetName = assetName, + assetQuantity = assetQuantity, + assetDisplayName = assetDisplayName, + eventSink = {}, + ) + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationState.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationState.kt new file mode 100644 index 0000000000..3f89b61a19 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationState.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import io.element.android.features.wallet.impl.slash.Lovelace + +/** + * State for the payment confirmation screen. + */ +data class PaymentConfirmationState( + val recipientAddress: String, + val recipientAddressDisplay: String, + val amountLovelace: Lovelace, + val amountAda: String, + val estimatedFeeLovelace: Lovelace?, + val estimatedFeeAda: String?, + val totalLovelace: Lovelace?, + val totalAda: String?, + val senderAddress: String, + val senderBalanceLovelace: Lovelace?, + val insufficientFunds: Boolean, + val isTestnet: Boolean, + val isFeeLoading: Boolean, + val feeError: String?, + /** True if sending a native asset (token). */ + val isSendingToken: Boolean, + /** Policy ID of the token being sent. */ + val assetPolicyId: String?, + /** Asset name (hex) of the token being sent. */ + val assetName: String?, + /** Quantity of the token being sent. */ + val assetQuantity: Long?, + /** Human-readable display name of the token. */ + val assetDisplayName: String?, + val eventSink: (PaymentFlowEvents) -> Unit, +) { + /** + * Formatted token quantity for display. + */ + val tokenQuantityDisplay: String? + get() = assetQuantity?.toString() + + companion object { + fun truncateAddress(address: String): String { + if (address.length <= 20) return address + return "${address.take(8)}...${address.takeLast(6)}" + } + + fun formatAda(lovelace: Lovelace): String { + val ada = lovelace / 1_000_000.0 + return String.format("%.6f", ada).trimEnd('0').trimEnd('.') + } + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationView.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationView.kt new file mode 100644 index 0000000000..be44116db8 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationView.kt @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import android.view.WindowManager +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Send +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +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.IconSource + +/** + * Payment confirmation screen. + * + * FLAG_SECURE is applied to prevent screenshots of transaction details. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PaymentConfirmationView( + state: PaymentConfirmationState, + onConfirm: () -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + // FLAG_SECURE to prevent screenshots of payment details + val view = LocalView.current + DisposableEffect(Unit) { + val window = (view.context as? android.app.Activity)?.window + window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + onDispose { window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } + } + + Scaffold( + modifier = modifier.fillMaxSize().systemBarsPadding().imePadding(), + topBar = { + TopAppBar( + title = { Text("Confirm Payment") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + } + ) + } + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 16.dp).verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (state.isTestnet) { TestnetWarningCard() } + Spacer(modifier = Modifier.height(8.dp)) + + // Amount card — show token or ADA + if (state.isSendingToken) { + TokenAmountCard( + tokenQuantity = state.tokenQuantityDisplay ?: "?", + tokenName = state.assetDisplayName ?: "Token", + accompanyingAda = state.amountAda, + ) + } else { + AmountCard(amountAda = state.amountAda) + } + + TransactionDetailsCard(state) + if (state.insufficientFunds) { + InsufficientFundsCard(balanceLovelace = state.senderBalanceLovelace, requiredLovelace = state.totalLovelace) + } + Spacer(modifier = Modifier.weight(1f)) + Button( + text = "Send", + onClick = { state.eventSink(PaymentFlowEvents.ConfirmPayment); onConfirm() }, + enabled = !state.isFeeLoading && !state.insufficientFunds, + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + leadingIcon = IconSource.Vector(Icons.Default.Send), + ) + } + } +} + +@Composable +private fun TestnetWarningCard(modifier: Modifier = Modifier) { + Card(modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer)) { + Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text("⚠️", style = MaterialTheme.typography.titleMedium) + Text("Testnet transaction — no real ADA", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onTertiaryContainer) + } + } +} + +@Composable +private fun AmountCard(amountAda: String, modifier: Modifier = Modifier) { + Card(modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)) { + Column(modifier = Modifier.fillMaxWidth().padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Text("Amount", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)) + Text("$amountAda ADA", style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onPrimaryContainer) + } + } +} + +/** + * Amount card for token sends — shows token quantity prominently with ADA amount below. + */ +@Composable +private fun TokenAmountCard( + tokenQuantity: String, + tokenName: String, + accompanyingAda: String, + modifier: Modifier = Modifier, +) { + Card(modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)) { + Column(modifier = Modifier.fillMaxWidth().padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Text("Amount", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)) + Text("$tokenQuantity $tokenName", style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onPrimaryContainer) + Text("+ $accompanyingAda ADA (min UTXO)", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)) + } + } +} + +@Composable +private fun TransactionDetailsCard(state: PaymentConfirmationState, modifier: Modifier = Modifier) { + Card(modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + DetailRow(label = "To", value = state.recipientAddressDisplay) + + // Show token info if sending a token + if (state.isSendingToken && state.assetDisplayName != null) { + HorizontalDivider() + DetailRow(label = "Token", value = state.assetDisplayName) + DetailRow(label = "Quantity", value = state.tokenQuantityDisplay ?: "?") + } + + HorizontalDivider() + DetailRow(label = "Network fee", value = if (state.isFeeLoading) null else state.estimatedFeeAda?.let { "~$it ADA" } ?: "Unknown", isLoading = state.isFeeLoading) + state.feeError?.let { Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error) } + HorizontalDivider() + DetailRow(label = "Total ADA", value = state.totalAda?.let { "$it ADA" } ?: "—", isBold = true) + } + } +} + +@Composable +private fun DetailRow(label: String, value: String?, isBold: Boolean = false, isLoading: Boolean = false, modifier: Modifier = Modifier) { + Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Text(label, style = if (isBold) MaterialTheme.typography.titleMedium else MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + if (isLoading) CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp) + else Text(value ?: "—", style = if (isBold) MaterialTheme.typography.titleMedium else MaterialTheme.typography.bodyMedium, fontWeight = if (isBold) FontWeight.Bold else FontWeight.Normal, color = MaterialTheme.colorScheme.onSurfaceVariant) + } +} + +@Composable +private fun InsufficientFundsCard(balanceLovelace: Long?, requiredLovelace: Long?, modifier: Modifier = Modifier) { + Card(modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("Insufficient funds", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onErrorContainer) + val balanceAda = balanceLovelace?.let { PaymentConfirmationState.formatAda(it) } ?: "?" + val requiredAda = requiredLovelace?.let { PaymentConfirmationState.formatAda(it) } ?: "?" + Text("You have $balanceAda ADA but need $requiredAda ADA (including fee)", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f)) + } + } +} + +@PreviewsDayNight +@Composable +internal fun PaymentConfirmationViewPreview(@PreviewParameter(PaymentConfirmationStateProvider::class) state: PaymentConfirmationState) { + ElementPreview { PaymentConfirmationView(state = state, onConfirm = {}, onBack = {}) } +} + +internal class PaymentConfirmationStateProvider : PreviewParameterProvider { + override val values = sequenceOf( + // ADA send + PaymentConfirmationState( + recipientAddress = "addr_test1qp2fg770ddmqxxduasjsas8rgimrhknmqjn43mj74g7ta2tjt0n5nh4t5xqf6lp5mwfpksj9csjg9s4kgfhvwj7m7dcq9qf7zj", + recipientAddressDisplay = "addr_tes...q9qf7zj", amountLovelace = 10_000_000L, amountAda = "10", + estimatedFeeLovelace = 180_000L, estimatedFeeAda = "0.18", totalLovelace = 10_180_000L, totalAda = "10.18", + senderAddress = "addr_test1q...", senderBalanceLovelace = 100_000_000L, insufficientFunds = false, + isTestnet = true, isFeeLoading = false, feeError = null, + isSendingToken = false, assetPolicyId = null, assetName = null, assetQuantity = null, assetDisplayName = null, + eventSink = {}, + ), + // Token send + PaymentConfirmationState( + recipientAddress = "addr_test1qp2fg770ddmqxxduasjsas8rgimrhknmqjn43mj74g7ta2tjt0n5nh4t5xqf6lp5mwfpksj9csjg9s4kgfhvwj7m7dcq9qf7zj", + recipientAddressDisplay = "addr_tes...q9qf7zj", amountLovelace = 1_500_000L, amountAda = "1.5", + estimatedFeeLovelace = 200_000L, estimatedFeeAda = "0.2", totalLovelace = 1_700_000L, totalAda = "1.7", + senderAddress = "addr_test1q...", senderBalanceLovelace = 100_000_000L, insufficientFunds = false, + isTestnet = true, isFeeLoading = false, feeError = null, + isSendingToken = true, assetPolicyId = "abc123", assetName = "484f534b59", assetQuantity = 1000L, assetDisplayName = "HOSKY", + eventSink = {}, + ), + ) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryNode.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryNode.kt new file mode 100644 index 0000000000..b78b4cbe98 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryNode.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.wallet.impl.cardano.DefaultTransactionBuilder +import io.element.android.features.wallet.impl.slash.ParsedPayCommand +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +@AssistedInject +class PaymentEntryNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenterFactory: PaymentEntryPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + @Parcelize + data class Inputs( + val roomId: RoomId, + val parsedCommand: ParsedPayCommand?, + ) : NodeInputs, Parcelable + + interface Callback : Plugin { + fun onContinue( + recipientAddress: String, + amountLovelace: Long, + assetPolicyId: String?, + assetName: String?, + assetQuantity: Long?, + assetDisplayName: String?, + ) + fun onCancel() + fun onOpenWalletSettings() + } + + private val inputs: Inputs = plugins.filterIsInstance().first() + private val callback: Callback = plugins.filterIsInstance().first() + + private val presenter by lazy { + presenterFactory.create( + roomId = inputs.roomId, + parsedCommand = inputs.parsedCommand, + ) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + + PaymentEntryView( + state = state, + onContinue = { + // Use the resolved Cardano address (from lookup or manual entry) + val recipientAddress = state.resolvedAddress ?: return@PaymentEntryView + + if (state.selectedAsset != null) { + // Token send — use minimum ADA for UTXO, pass token details + val asset = state.selectedAsset + callback.onContinue( + recipientAddress = recipientAddress, + amountLovelace = DefaultTransactionBuilder.MIN_TOKEN_UTXO_LOVELACE, + assetPolicyId = asset.policyId, + assetName = asset.assetName, + assetQuantity = state.parsedTokenAmount, + assetDisplayName = asset.name, + ) + } else { + // ADA-only send + val amount = state.parsedAmountLovelace ?: return@PaymentEntryView + callback.onContinue( + recipientAddress = recipientAddress, + amountLovelace = amount, + assetPolicyId = null, + assetName = null, + assetQuantity = null, + assetDisplayName = null, + ) + } + }, + onCancel = { + callback.onCancel() + }, + onOpenWalletSettings = { + callback.onOpenWalletSettings() + }, + modifier = modifier, + ) + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryPresenter.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryPresenter.kt new file mode 100644 index 0000000000..88fe4eeb98 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryPresenter.kt @@ -0,0 +1,470 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +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.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import io.element.android.features.wallet.api.CardanoClient +import io.element.android.features.wallet.api.NativeAsset +import io.element.android.features.wallet.api.address.CardanoAddressService +import io.element.android.features.wallet.impl.cardano.CardanoNetwork +import io.element.android.features.wallet.impl.cardano.CardanoNetworkConfig +import io.element.android.features.wallet.impl.cardano.CardanoWalletManager +import io.element.android.features.wallet.impl.slash.Lovelace +import io.element.android.features.wallet.impl.slash.ParsedPayCommand +import io.element.android.libraries.architecture.Presenter +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 timber.log.Timber +import java.math.BigDecimal + +/** + * Presenter for the payment entry screen. + */ +class PaymentEntryPresenter @AssistedInject constructor( + @Assisted private val roomId: RoomId, + @Assisted private val parsedCommand: ParsedPayCommand?, + private val matrixClient: MatrixClient, + private val walletManager: CardanoWalletManager, + private val cardanoClient: CardanoClient, + private val cardanoAddressService: CardanoAddressService, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(roomId: RoomId, parsedCommand: ParsedPayCommand?): PaymentEntryPresenter + } + + companion object { + private const val TAG = "PaymentEntryPresenter" + private const val LOVELACE_PER_ADA = 1_000_000L + private const val MIN_AMOUNT_LOVELACE = 1_000_000L + private const val MAX_ADA_SUPPLY = 45_000_000_000L + private val CARDANO_ADDRESS_REGEX = "^addr(_test)?1[a-zA-Z0-9]+$".toRegex() + private val MATRIX_USER_REGEX = "^@[a-zA-Z0-9._=-]+:[a-zA-Z0-9.-]+$".toRegex() + // ADA Handle: $handle format with alphanumeric, underscore, dash, period + private val HANDLE_REGEX = "^\\\$[a-zA-Z0-9_.-]+$".toRegex() + } + + @Composable + override fun present(): PaymentEntryState { + val walletState by walletManager.walletState.collectAsState() + var walletInitialized by remember { mutableStateOf(false) } + + // Initialize wallet manager first + LaunchedEffect(Unit) { + val sessionId = matrixClient.sessionId + walletManager.initialize(sessionId) + walletInitialized = true + } + + // Show loading state while checking wallet + if (!walletInitialized || walletState.isLoading) { + return PaymentEntryState.Loading + } + + // If no wallet is set up, return early with that state + if (!walletState.hasWallet) { + return PaymentEntryState( + noWalletSetup = true, + isCheckingWallet = false, + amountInput = "", + recipientInput = "", + manualAddressInput = "", + prefillAmount = null, + prefillRecipient = null, + parsedAmountLovelace = null, + isValidRecipient = false, + recipientResolutionState = RecipientResolutionState.NotNeeded, + resolvedAddress = null, + senderAddress = null, + senderBalanceAda = null, + isTestnet = CardanoNetworkConfig.NETWORK == CardanoNetwork.TESTNET, + amountError = null, + recipientError = null, + manualAddressError = null, + canContinue = false, + selectedAsset = null, + availableAssets = emptyList(), + tokenAmountInput = "", + parsedTokenAmount = null, + tokenAmountError = null, + eventSink = {}, + ) + } + + // User has a wallet — proceed with normal payment flow + val (prefillAmount, prefillRecipient) = remember(parsedCommand) { + extractPrefills(parsedCommand) + } + + var amountInput by remember { mutableStateOf(prefillAmount?.let { formatLovelaceInput(it) } ?: "") } + var recipientInput by remember { mutableStateOf(prefillRecipient ?: "") } + var manualAddressInput by remember { mutableStateOf("") } + var senderAddress by remember { mutableStateOf(null) } + var senderBalanceLovelace by remember { mutableStateOf(null) } + var recipientResolutionState by remember { mutableStateOf(RecipientResolutionState.NotNeeded) } + // Track resolved address separately so we can use it for validation + var resolvedCardanoAddress by remember { mutableStateOf(null) } + + // Asset selection state + var selectedAsset by remember { mutableStateOf(null) } + var availableAssets by remember { mutableStateOf>(emptyList()) } + var tokenAmountInput by remember { mutableStateOf("") } + + LaunchedEffect(walletInitialized) { + if (walletInitialized) { + val sessionId = matrixClient.sessionId + senderAddress = walletManager.getAddress(sessionId).getOrNull() + senderAddress?.let { address -> + // Get balance + cardanoClient.getBalance(address).onSuccess { balance -> + senderBalanceLovelace = balance + } + // Get available assets + cardanoClient.getAddressAssets(address).onSuccess { assets -> + availableAssets = assets + Timber.tag(TAG).d("Loaded ${assets.size} native assets") + } + } + } + } + + // Look up Cardano address when a Matrix user or ADA Handle is entered + LaunchedEffect(recipientInput) { + val isMatrixUser = MATRIX_USER_REGEX.matches(recipientInput) + val isCardanoAddress = CARDANO_ADDRESS_REGEX.matches(recipientInput) + val isHandle = HANDLE_REGEX.matches(recipientInput) + + when { + recipientInput.isBlank() -> { + recipientResolutionState = RecipientResolutionState.NotNeeded + resolvedCardanoAddress = null + } + isCardanoAddress -> { + recipientResolutionState = RecipientResolutionState.NotNeeded + resolvedCardanoAddress = recipientInput + // Clear manual entry when direct address is entered + manualAddressInput = "" + } + isHandle -> { + // ADA Handle resolution + val handleName = recipientInput.removePrefix("$") + recipientResolutionState = RecipientResolutionState.Resolving(recipientInput) + resolvedCardanoAddress = null + + Timber.tag(TAG).d("Resolving ADA Handle: $recipientInput...") + + cardanoClient.resolveHandle(handleName) + .onSuccess { address -> + if (address != null) { + Timber.tag(TAG).i("Resolved $recipientInput -> $address") + recipientResolutionState = RecipientResolutionState.HandleResolved( + handle = recipientInput, + address = address + ) + resolvedCardanoAddress = address + } else { + Timber.tag(TAG).d("Handle $recipientInput not found") + recipientResolutionState = RecipientResolutionState.Error( + "Handle $recipientInput not found" + ) + } + } + .onFailure { e -> + Timber.tag(TAG).w(e, "Failed to resolve handle $recipientInput") + recipientResolutionState = RecipientResolutionState.Error( + "Failed to resolve handle: ${e.message}" + ) + } + } + isMatrixUser -> { + // Start lookup + recipientResolutionState = RecipientResolutionState.Resolving(recipientInput) + resolvedCardanoAddress = null + + Timber.tag(TAG).d("Looking up Cardano address for $recipientInput...") + + val userId = UserId(recipientInput) + cardanoAddressService.lookupAddress(userId) + .onSuccess { address -> + if (address != null) { + Timber.tag(TAG).i("Found Cardano address for $recipientInput") + recipientResolutionState = RecipientResolutionState.Found( + matrixUserId = recipientInput, + address = address + ) + resolvedCardanoAddress = address + } else { + Timber.tag(TAG).d("No Cardano address linked for $recipientInput") + recipientResolutionState = RecipientResolutionState.NeedsManualEntry( + matrixUserId = recipientInput, + displayName = null + ) + // Don't set resolvedCardanoAddress - user must enter manually + } + } + .onFailure { e -> + Timber.tag(TAG).w(e, "Failed to lookup address for $recipientInput") + recipientResolutionState = RecipientResolutionState.NeedsManualEntry( + matrixUserId = recipientInput, + displayName = null + ) + } + } + else -> { + recipientResolutionState = RecipientResolutionState.NotNeeded + resolvedCardanoAddress = null + } + } + } + + // When in manual entry mode, validate and use the manual address + val needsManualEntry = recipientResolutionState is RecipientResolutionState.NeedsManualEntry + val manualAddressError = if (needsManualEntry && manualAddressInput.isNotBlank()) { + validateManualAddress(manualAddressInput) + } else { + null + } + + // If manual address is valid, use it as the resolved address + val finalResolvedAddress = when { + needsManualEntry && manualAddressInput.isNotBlank() && manualAddressError == null -> manualAddressInput + else -> resolvedCardanoAddress + } + + // Amount validation depends on whether we're sending a token + val parsedAmountLovelace = parseAmountInput(amountInput) + val amountError = if (selectedAsset != null) { + // For token sends, ADA amount field is hidden but we still validate min UTXO + null + } else { + validateAmount(parsedAmountLovelace, amountInput) + } + + // Token amount validation + val parsedTokenAmount = parseTokenAmount(tokenAmountInput, selectedAsset) + val tokenAmountError = validateTokenAmount(parsedTokenAmount, tokenAmountInput, selectedAsset) + + val isCardanoAddress = CARDANO_ADDRESS_REGEX.matches(recipientInput) + val isMatrixUser = MATRIX_USER_REGEX.matches(recipientInput) + val isHandle = HANDLE_REGEX.matches(recipientInput) + val recipientError = validateRecipient(recipientInput, isCardanoAddress, isMatrixUser, isHandle, recipientResolutionState) + + // Recipient is valid if we have a final resolved address + val isValidRecipient = finalResolvedAddress != null + + // Can continue logic + val canContinue = if (selectedAsset != null) { + // Token send: need valid token amount and recipient + parsedTokenAmount != null && + parsedTokenAmount > 0 && + tokenAmountError == null && + isValidRecipient && + (recipientError == null || needsManualEntry) + } else { + // ADA send: need valid ADA amount and recipient + parsedAmountLovelace != null && + parsedAmountLovelace >= MIN_AMOUNT_LOVELACE && + amountError == null && + isValidRecipient && + (recipientError == null || needsManualEntry) + } + + fun handleEvent(event: PaymentFlowEvents) { + when (event) { + is PaymentFlowEvents.AmountChanged -> amountInput = event.amount + is PaymentFlowEvents.RecipientChanged -> { + recipientInput = event.recipient + // Clear resolved address and manual entry when input changes + resolvedCardanoAddress = null + manualAddressInput = "" + } + is PaymentFlowEvents.ManualAddressChanged -> { + manualAddressInput = event.address + } + is PaymentFlowEvents.AssetSelected -> { + selectedAsset = event.asset + // Clear token amount when switching assets + tokenAmountInput = "" + } + is PaymentFlowEvents.TokenAmountChanged -> { + tokenAmountInput = event.amount + } + else -> Unit + } + } + + val senderBalanceAda = senderBalanceLovelace?.let { balance -> + String.format("%.6f", balance / 1_000_000.0).trimEnd('0').trimEnd('.') + } + + return PaymentEntryState( + noWalletSetup = false, + isCheckingWallet = false, + amountInput = amountInput, + recipientInput = recipientInput, + manualAddressInput = manualAddressInput, + prefillAmount = prefillAmount, + prefillRecipient = prefillRecipient, + parsedAmountLovelace = parsedAmountLovelace, + isValidRecipient = isValidRecipient, + recipientResolutionState = recipientResolutionState, + resolvedAddress = finalResolvedAddress, + senderAddress = senderAddress, + senderBalanceAda = senderBalanceAda, + isTestnet = CardanoNetworkConfig.NETWORK == CardanoNetwork.TESTNET, + amountError = amountError, + recipientError = if (needsManualEntry) null else recipientError, // Hide error in manual entry mode + manualAddressError = manualAddressError, + canContinue = canContinue, + selectedAsset = selectedAsset, + availableAssets = availableAssets, + tokenAmountInput = tokenAmountInput, + parsedTokenAmount = parsedTokenAmount, + tokenAmountError = tokenAmountError, + eventSink = ::handleEvent, + ) + } + + private fun extractPrefills(command: ParsedPayCommand?): Pair { + return when (command) { + is ParsedPayCommand.WithAddressRecipient -> command.amount to command.address + is ParsedPayCommand.WithMatrixRecipient -> command.amount to command.matrixUserId.value + is ParsedPayCommand.AmountOnly -> command.amount to null + else -> null to null + } + } + + private fun formatLovelaceInput(lovelace: Lovelace): String { + val ada = lovelace / 1_000_000.0 + return String.format("%.6f", ada).trimEnd('0').trimEnd('.') + } + + private fun parseAmountInput(input: String): Lovelace? { + if (input.isBlank()) return null + return try { + val decimal = BigDecimal(input.trim()) + if (decimal <= BigDecimal.ZERO) return null + val lovelace = decimal.multiply(BigDecimal(LOVELACE_PER_ADA)) + lovelace.toLong() + } catch (e: Exception) { + null + } + } + + private fun parseTokenAmount(input: String, asset: NativeAsset?): Long? { + if (asset == null || input.isBlank()) return null + return try { + val decimal = BigDecimal(input.trim()) + if (decimal <= BigDecimal.ZERO) return null + + // Apply decimals if the token has them + val decimals = asset.decimals ?: 0 + if (decimals > 0) { + val multiplier = BigDecimal.TEN.pow(decimals) + decimal.multiply(multiplier).toLong() + } else { + decimal.toLong() + } + } catch (e: Exception) { + null + } + } + + private fun validateAmount(lovelace: Lovelace?, input: String): String? { + if (input.isBlank()) return null + if (lovelace == null) return "Invalid amount" + if (lovelace < MIN_AMOUNT_LOVELACE) return "Minimum amount is 1 ADA" + if (lovelace > MAX_ADA_SUPPLY * LOVELACE_PER_ADA) return "Amount too large" + return null + } + + private fun validateTokenAmount(amount: Long?, input: String, asset: NativeAsset?): String? { + if (asset == null) return null // Not sending a token + if (input.isBlank()) return null + if (amount == null) return "Invalid amount" + if (amount <= 0) return "Amount must be positive" + if (amount > asset.quantity) return "Insufficient balance (have ${asset.formatQuantity()})" + return null + } + + private fun validateRecipient( + input: String, + isCardanoAddress: Boolean, + isMatrixUser: Boolean, + isHandle: Boolean, + resolutionState: RecipientResolutionState + ): String? { + if (input.isBlank()) return null + + // ADA Handle with ongoing resolution + if (isHandle) { + return when (resolutionState) { + is RecipientResolutionState.Resolving -> null // Still resolving + is RecipientResolutionState.HandleResolved -> null // Found address + is RecipientResolutionState.Error -> resolutionState.message + else -> null + } + } + + // Matrix user with ongoing resolution + if (isMatrixUser) { + return when (resolutionState) { + is RecipientResolutionState.Resolving -> null // Still looking up + is RecipientResolutionState.Found -> null // Found address + is RecipientResolutionState.NeedsManualEntry -> null // Will use manual entry field + is RecipientResolutionState.Error -> resolutionState.message + else -> null + } + } + + if (!isCardanoAddress && !isMatrixUser && !isHandle) { + return "Enter a Cardano address (addr1...), Matrix user (@user:server), or ADA Handle (\$handle)" + } + if (isCardanoAddress && input.length < 50) { + return "Address too short" + } + return null + } + + private fun validateManualAddress(input: String): String? { + if (input.isBlank()) return null + + // Must start with addr_test1 (preprod) or addr1 (mainnet) + val isTestnet = input.startsWith("addr_test1") + val isMainnet = input.startsWith("addr1") && !input.startsWith("addr_test1") + + if (!isTestnet && !isMainnet) { + return "Address must start with addr1 or addr_test1" + } + + // Length check: Cardano addresses are typically 58-108 characters + if (input.length < 58) { + return "Address too short" + } + if (input.length > 108) { + return "Address too long" + } + + // Basic character validation + if (!CARDANO_ADDRESS_REGEX.matches(input)) { + return "Invalid Cardano address format" + } + + return null + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryState.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryState.kt new file mode 100644 index 0000000000..ff5b81eaa5 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryState.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import io.element.android.features.wallet.api.NativeAsset +import io.element.android.features.wallet.impl.slash.Lovelace + +/** + * State for the payment entry screen. + */ +data class PaymentEntryState( + /** True if the user has no wallet set up yet. */ + val noWalletSetup: Boolean, + /** True while checking if wallet exists. */ + val isCheckingWallet: Boolean, + val amountInput: String, + val recipientInput: String, + /** Manual address entry field - shown when recipient has no linked wallet. */ + val manualAddressInput: String, + val prefillAmount: Lovelace?, + val prefillRecipient: String?, + val parsedAmountLovelace: Lovelace?, + val isValidRecipient: Boolean, + val recipientResolutionState: RecipientResolutionState, + /** The final resolved Cardano address to use for the transaction. */ + val resolvedAddress: String?, + val senderAddress: String?, + val senderBalanceAda: String?, + val isTestnet: Boolean, + val amountError: String?, + val recipientError: String?, + /** Validation error for manual address entry field. */ + val manualAddressError: String?, + val canContinue: Boolean, + /** Currently selected asset (null = ADA). */ + val selectedAsset: NativeAsset?, + /** Available native assets in the wallet. */ + val availableAssets: List, + /** Token amount input (when sending a native asset). */ + val tokenAmountInput: String, + /** Parsed token amount. */ + val parsedTokenAmount: Long?, + /** Token amount validation error. */ + val tokenAmountError: String?, + val eventSink: (PaymentFlowEvents) -> Unit, +) { + val parsedAmountAda: String? + get() = parsedAmountLovelace?.let { lovelace -> + val ada = lovelace / 1_000_000.0 + String.format("%.6f", ada).trimEnd('0').trimEnd('.') + } + + /** True when the user must manually enter an address for the recipient. */ + val needsManualAddressEntry: Boolean + get() = recipientResolutionState is RecipientResolutionState.NeedsManualEntry + + /** True if sending a native asset (token) instead of ADA. */ + val isSendingToken: Boolean + get() = selectedAsset != null + + /** Display name for the selected asset (or "ADA"). */ + val selectedAssetName: String + get() = selectedAsset?.name ?: "ADA" + + companion object { + /** Initial loading state while checking wallet. */ + val Loading = PaymentEntryState( + noWalletSetup = false, + isCheckingWallet = true, + amountInput = "", + recipientInput = "", + manualAddressInput = "", + prefillAmount = null, + prefillRecipient = null, + parsedAmountLovelace = null, + isValidRecipient = false, + recipientResolutionState = RecipientResolutionState.NotNeeded, + resolvedAddress = null, + senderAddress = null, + senderBalanceAda = null, + isTestnet = false, + amountError = null, + recipientError = null, + manualAddressError = null, + canContinue = false, + selectedAsset = null, + availableAssets = emptyList(), + tokenAmountInput = "", + parsedTokenAmount = null, + tokenAmountError = null, + eventSink = {}, + ) + } +} + +/** + * State of resolving a Matrix user ID or ADA Handle to a Cardano address. + */ +sealed interface RecipientResolutionState { + /** Not a Matrix user ID or ADA Handle - no resolution needed. */ + data object NotNeeded : RecipientResolutionState + + /** Currently looking up the user's Cardano address. */ + data class Resolving(val matrixUserId: String) : RecipientResolutionState + + /** Found the user's Cardano address from their Matrix profile. */ + data class Found( + val matrixUserId: String, + val address: String, + val displayName: String? = null + ) : RecipientResolutionState + + /** User has no Cardano address linked - needs manual entry. */ + data class NeedsManualEntry(val matrixUserId: String, val displayName: String?) : RecipientResolutionState + + /** Successfully resolved to a Cardano address (manual entry or from lookup). */ + data class Resolved(val address: String) : RecipientResolutionState + + /** Resolved from ADA Handle ($handle). */ + data class HandleResolved( + val handle: String, + val address: String, + ) : RecipientResolutionState + + /** Failed to look up address. */ + data class Error(val message: String) : RecipientResolutionState +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryView.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryView.kt new file mode 100644 index 0000000000..ed2f39a940 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryView.kt @@ -0,0 +1,655 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.wallet.api.NativeAsset +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 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PaymentEntryView( + state: PaymentEntryState, + onContinue: () -> Unit, + onCancel: () -> Unit, + onOpenWalletSettings: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + topBar = { + TopAppBar( + title = { Text("Send Payment") }, + navigationIcon = { + IconButton(onClick = onCancel) { + Icon(Icons.Default.Close, contentDescription = "Cancel") + } + } + ) + } + ) { padding -> + when { + state.isCheckingWallet -> { + // Loading state + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } + state.noWalletSetup -> { + // No wallet setup prompt + NoWalletSetupContent( + onOpenWalletSettings = onOpenWalletSettings, + onCancel = onCancel, + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 24.dp), + ) + } + else -> { + // Normal payment form + PaymentFormContent( + state = state, + onContinue = onContinue, + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 16.dp), + ) + } + } + } +} + +@Composable +private fun NoWalletSetupContent( + onOpenWalletSettings: () -> Unit, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + // Cardano icon + Text( + text = "₳", + style = MaterialTheme.typography.displayLarge, + color = MaterialTheme.colorScheme.primary, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Wallet Required", + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = "You need to set up a Cardano wallet before you can send payments.", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Button( + text = "Open Wallet Settings", + onClick = onOpenWalletSettings, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Button( + text = "Cancel", + onClick = onCancel, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun PaymentFormContent( + state: PaymentEntryState, + onContinue: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (state.isTestnet) { + TestnetWarningCard() + } + + state.senderBalanceAda?.let { balance -> + BalanceInfoCard(balanceAda = balance) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Asset selector (if there are tokens available) + if (state.availableAssets.isNotEmpty()) { + AssetSelector( + selectedAsset = state.selectedAsset, + availableAssets = state.availableAssets, + onAssetSelected = { state.eventSink(PaymentFlowEvents.AssetSelected(it)) }, + ) + } + + // Amount input — different based on selected asset + if (state.selectedAsset != null) { + // Token amount input + OutlinedTextField( + value = state.tokenAmountInput, + onValueChange = { state.eventSink(PaymentFlowEvents.TokenAmountChanged(it)) }, + label = { Text("Amount (${state.selectedAsset.name})") }, + placeholder = { Text("0") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + isError = state.tokenAmountError != null, + supportingText = if (state.tokenAmountError != null) { + { Text(state.tokenAmountError, color = MaterialTheme.colorScheme.error) } + } else { + { Text("Available: ${state.selectedAsset.formatQuantity()}") } + }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + + // Note about min ADA + Text( + text = "Note: Token sends include ~1.5 ADA for Cardano protocol requirements", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + // ADA amount input + OutlinedTextField( + value = state.amountInput, + onValueChange = { state.eventSink(PaymentFlowEvents.AmountChanged(it)) }, + label = { Text("Amount (ADA)") }, + placeholder = { Text("0.00") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + isError = state.amountError != null, + supportingText = state.amountError?.let { { Text(it, color = MaterialTheme.colorScheme.error) } }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + } + + OutlinedTextField( + value = state.recipientInput, + onValueChange = { state.eventSink(PaymentFlowEvents.RecipientChanged(it)) }, + label = { Text("Recipient") }, + placeholder = { Text("addr1..., @user:server, or \$handle") }, + isError = state.recipientError != null, + supportingText = state.recipientError?.let { { Text(it, color = MaterialTheme.colorScheme.error) } }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + + // Show resolution state feedback + when (val resolution = state.recipientResolutionState) { + is RecipientResolutionState.Resolving -> { + AddressLookupInProgressCard(matrixUserId = resolution.matrixUserId) + } + is RecipientResolutionState.Found -> { + AddressFoundCard( + matrixUserId = resolution.matrixUserId, + address = resolution.address, + ) + } + is RecipientResolutionState.HandleResolved -> { + HandleResolvedCard( + handle = resolution.handle, + address = resolution.address, + ) + } + is RecipientResolutionState.NeedsManualEntry -> { + ManualAddressEntryCard( + matrixUserId = resolution.matrixUserId, + displayName = resolution.displayName, + manualAddressInput = state.manualAddressInput, + manualAddressError = state.manualAddressError, + onManualAddressChanged = { state.eventSink(PaymentFlowEvents.ManualAddressChanged(it)) }, + ) + } + is RecipientResolutionState.Error -> { + Text( + resolution.message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } + else -> Unit + } + + Spacer(modifier = Modifier.weight(1f)) + + Button( + text = "Continue", + onClick = { + state.eventSink(PaymentFlowEvents.Continue) + onContinue() + }, + enabled = state.canContinue, + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + ) + } +} + +/** + * Asset selector dropdown. + */ +@Composable +private fun AssetSelector( + selectedAsset: NativeAsset?, + availableAssets: List, + onAssetSelected: (NativeAsset?) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + ) { + Box { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = true } + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Text( + "Asset", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + selectedAsset?.name ?: "ADA", + style = MaterialTheme.typography.bodyLarge, + ) + } + Icon( + Icons.Default.KeyboardArrowDown, + contentDescription = "Select asset", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + // ADA option + DropdownMenuItem( + text = { + Text("ADA") + }, + onClick = { + onAssetSelected(null) + expanded = false + } + ) + + // Available tokens + availableAssets.forEach { asset -> + DropdownMenuItem( + text = { + Column { + Text(asset.name) + Text( + "Balance: ${asset.formatQuantity()}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + onClick = { + onAssetSelected(asset) + expanded = false + } + ) + } + } + } + } +} + +@Composable +private fun TestnetWarningCard(modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer), + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text("⚠️", style = MaterialTheme.typography.titleMedium) + Text( + "Testnet transaction — no real ADA", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer + ) + } + } +} + +@Composable +private fun BalanceInfoCard(balanceAda: String, modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + "Available balance", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + "$balanceAda ADA", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun AddressLookupInProgressCard(matrixUserId: String, modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp) + Text( + text = "Looking up address for $matrixUserId...", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Composable +private fun AddressFoundCard(matrixUserId: String, address: String, modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer), + ) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = CompoundIcons.Check(), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary, + ) + val displayName = matrixUserId.substringBefore(":").removePrefix("@") + Text( + text = "Address loaded from $displayName's profile", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + // Show truncated address + val truncatedAddress = if (address.length > 24) { + "${address.take(12)}...${address.takeLast(8)}" + } else { + address + } + Text( + text = truncatedAddress, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f), + ) + } + } +} + +/** + * Card shown when an ADA Handle has been resolved to an address. + */ +@Composable +private fun HandleResolvedCard(handle: String, address: String, modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer), + ) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = CompoundIcons.Check(), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = "Resolved from $handle ✓", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + // Show truncated address + val truncatedAddress = if (address.length > 24) { + "${address.take(12)}...${address.takeLast(8)}" + } else { + address + } + Text( + text = truncatedAddress, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f), + ) + } + } +} + +/** + * Card shown when the Matrix user has no linked Cardano wallet. + * Includes a text field for manual address entry. + */ +@Composable +private fun ManualAddressEntryCard( + matrixUserId: String, + displayName: String?, + manualAddressInput: String, + manualAddressError: String?, + onManualAddressChanged: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer), + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Warning header + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text("⚠️", style = MaterialTheme.typography.titleMedium) + val name = displayName ?: matrixUserId.substringBefore(":").removePrefix("@") + Text( + text = "$name hasn't linked a Cardano wallet", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer, + ) + } + + Text( + text = "Enter their Cardano address manually:", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f), + ) + + // Manual address entry field + OutlinedTextField( + value = manualAddressInput, + onValueChange = onManualAddressChanged, + placeholder = { Text("addr1... or addr_test1...") }, + isError = manualAddressError != null, + supportingText = if (manualAddressError != null) { + { Text(manualAddressError, color = MaterialTheme.colorScheme.error) } + } else if (manualAddressInput.isNotBlank()) { + { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + imageVector = CompoundIcons.Check(), + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text("Valid address", color = MaterialTheme.colorScheme.primary) + } + } + } else { + null + }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun PaymentEntryViewPreview( + @PreviewParameter(PaymentEntryStateProvider::class) state: PaymentEntryState +) { + ElementPreview { + PaymentEntryView(state = state, onContinue = {}, onCancel = {}, onOpenWalletSettings = {}) + } +} + +internal class PaymentEntryStateProvider : PreviewParameterProvider { + override val values = sequenceOf( + // Normal state with wallet + PaymentEntryState( + noWalletSetup = false, isCheckingWallet = false, + amountInput = "", recipientInput = "", manualAddressInput = "", + prefillAmount = null, prefillRecipient = null, + parsedAmountLovelace = null, isValidRecipient = false, + recipientResolutionState = RecipientResolutionState.NotNeeded, + resolvedAddress = null, + senderAddress = "addr_test1qp2fg...", senderBalanceAda = "100.5", isTestnet = true, + amountError = null, recipientError = null, manualAddressError = null, + canContinue = false, selectedAsset = null, availableAssets = emptyList(), + tokenAmountInput = "", parsedTokenAmount = null, tokenAmountError = null, + eventSink = {}, + ), + // With available tokens + PaymentEntryState( + noWalletSetup = false, isCheckingWallet = false, + amountInput = "", recipientInput = "", manualAddressInput = "", + prefillAmount = null, prefillRecipient = null, + parsedAmountLovelace = null, isValidRecipient = false, + recipientResolutionState = RecipientResolutionState.NotNeeded, + resolvedAddress = null, + senderAddress = "addr_test1qp2fg...", senderBalanceAda = "100.5", isTestnet = true, + amountError = null, recipientError = null, manualAddressError = null, + canContinue = false, selectedAsset = null, + availableAssets = listOf( + NativeAsset("abc123", "484f534b59", 1000000L, "HOSKY", null), + NativeAsset("def456", "4d494e", 500L, "MIN", null), + ), + tokenAmountInput = "", parsedTokenAmount = null, tokenAmountError = null, + eventSink = {}, + ), + // Loading state + PaymentEntryState.Loading, + ) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentFlowEvents.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentFlowEvents.kt new file mode 100644 index 0000000000..e4adbc26d5 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentFlowEvents.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import io.element.android.features.wallet.api.NativeAsset + +/** + * Events for the payment flow state machine. + */ +sealed interface PaymentFlowEvents { + // Entry screen events + data class AmountChanged(val amount: String) : PaymentFlowEvents + data class RecipientChanged(val recipient: String) : PaymentFlowEvents + /** Manual address entry when recipient has no linked wallet. */ + data class ManualAddressChanged(val address: String) : PaymentFlowEvents + /** Select an asset to send (null = ADA). */ + data class AssetSelected(val asset: NativeAsset?) : PaymentFlowEvents + /** Token amount changed (when sending a native asset). */ + data class TokenAmountChanged(val amount: String) : PaymentFlowEvents + data object Continue : PaymentFlowEvents + data object Cancel : PaymentFlowEvents + + // Confirmation screen events + data object ConfirmPayment : PaymentFlowEvents + data object GoBack : PaymentFlowEvents + + // Authentication events + data class AuthenticationResult(val success: Boolean, val errorMessage: String? = null) : PaymentFlowEvents + + // Progress screen events + data object Done : PaymentFlowEvents + data object RetryPayment : PaymentFlowEvents + data object ViewOnExplorer : PaymentFlowEvents +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressNode.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressNode.kt new file mode 100644 index 0000000000..cf74099113 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressNode.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.wallet.impl.slash.Lovelace +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.parcelize.Parcelize + +/** + * Node for the payment progress screen. + * + * Displays transaction submission progress and polls for confirmation. + */ +@ContributesNode(SessionScope::class) +class PaymentProgressNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenterFactory: PaymentProgressPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + @Parcelize + data class Inputs( + val recipientAddress: String, + val amountLovelace: Lovelace, + val roomId: RoomId, + val assetPolicyId: String?, + val assetName: String?, + val assetQuantity: Long?, + val assetDisplayName: String?, + ) : NodeInputs, Parcelable + + interface Callback : Plugin { + fun onPaymentComplete(txHash: String?) + fun onRetry() + } + + private val inputs: Inputs = plugins.filterIsInstance().first() + private val callback: Callback = plugins.filterIsInstance().first() + + private val presenter by lazy { + presenterFactory.create( + recipientAddress = inputs.recipientAddress, + amountLovelace = inputs.amountLovelace, + roomId = inputs.roomId, + assetPolicyId = inputs.assetPolicyId, + assetName = inputs.assetName, + assetQuantity = inputs.assetQuantity, + assetDisplayName = inputs.assetDisplayName, + ) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + + PaymentProgressView( + state = state, + onDone = { + callback.onPaymentComplete(state.txHash) + }, + onRetry = { + callback.onRetry() + }, + modifier = modifier, + ) + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressPresenter.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressPresenter.kt new file mode 100644 index 0000000000..b2ef279673 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressPresenter.kt @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import io.element.android.features.wallet.api.CardanoClient +import io.element.android.features.wallet.api.PaymentEventSender +import io.element.android.features.wallet.api.PaymentRequest +import io.element.android.features.wallet.api.PaymentStatusPoller +import io.element.android.features.wallet.api.SignedTransaction +import io.element.android.features.wallet.api.TransactionBuilder +import io.element.android.features.wallet.api.TxStatus +import io.element.android.features.wallet.impl.cardano.CardanoNetwork +import io.element.android.features.wallet.impl.cardano.CardanoNetworkConfig +import io.element.android.features.wallet.impl.cardano.CardanoWalletManager +import io.element.android.features.wallet.impl.slash.Lovelace +import io.element.android.libraries.architecture.Presenter +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 timber.log.Timber + +/** + * Presenter for the payment progress screen. + */ +class PaymentProgressPresenter @AssistedInject constructor( + @Assisted private val recipientAddress: String, + @Assisted private val amountLovelace: Lovelace, + @Assisted private val roomId: RoomId, + @Assisted private val assetPolicyId: String?, + @Assisted private val assetName: String?, + @Assisted private val assetQuantity: Long?, + @Assisted private val assetDisplayName: String?, + private val matrixClient: MatrixClient, + private val walletManager: CardanoWalletManager, + private val transactionBuilder: TransactionBuilder, + private val cardanoClient: CardanoClient, + private val paymentStatusPoller: PaymentStatusPoller, + private val paymentEventSender: PaymentEventSender, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create( + recipientAddress: String, + amountLovelace: Lovelace, + roomId: RoomId, + assetPolicyId: String?, + assetName: String?, + assetQuantity: Long?, + assetDisplayName: String?, + ): PaymentProgressPresenter + } + + companion object { + private const val TAG = "PaymentProgressPresenter" + private const val TIMEOUT_THRESHOLD_MS = 10 * 60 * 1000L + } + + private val isSendingToken: Boolean + get() = assetPolicyId != null && assetQuantity != null + + @Composable + override fun present(): PaymentProgressState { + val sessionId = matrixClient.sessionId + + var txHash by remember { mutableStateOf(null) } + var txStatus by remember { mutableStateOf(TxStatus.PENDING) } + var submissionState by remember { mutableStateOf(SubmissionState.Submitting) } + var errorMessage by remember { mutableStateOf(null) } + var submissionStartTime by remember { mutableStateOf(0L) } + + // Store for event sending + var lastRequest by remember { mutableStateOf(null) } + var lastSignedTx by remember { mutableStateOf(null) } + var eventSent by remember { mutableStateOf(false) } + + // Build and submit + LaunchedEffect(Unit) { + submissionStartTime = System.currentTimeMillis() + submissionState = SubmissionState.Submitting + + val senderAddress = walletManager.getAddress(sessionId).getOrNull() + if (senderAddress == null) { + submissionState = SubmissionState.Failed("Could not get wallet address") + errorMessage = "Failed to load wallet address" + return@LaunchedEffect + } + + val request = PaymentRequest( + fromAddress = senderAddress, + toAddress = recipientAddress, + amountLovelace = amountLovelace, + sessionId = sessionId, + assetPolicyId = assetPolicyId, + assetName = assetName, + assetQuantity = assetQuantity, + ) + + if (isSendingToken) { + Timber.tag(TAG).d("Building token tx: $assetQuantity $assetDisplayName to ${recipientAddress.take(20)}...") + } else { + Timber.tag(TAG).d("Building ADA tx: $amountLovelace lovelace to ${recipientAddress.take(20)}...") + } + + transactionBuilder.buildAndSign(request).onSuccess { signedTx -> + Timber.tag(TAG).d("Transaction built successfully, hash: ${signedTx.txHash}") + txHash = signedTx.txHash + lastRequest = request + lastSignedTx = signedTx + + cardanoClient.submitTx(signedTx.txCbor).onSuccess { submittedHash -> + Timber.tag(TAG).i("Transaction submitted: $submittedHash") + submissionState = SubmissionState.Pending + }.onFailure { error -> + Timber.tag(TAG).e(error, "Failed to submit transaction") + submissionState = SubmissionState.Failed("Failed to submit transaction") + errorMessage = error.message ?: "Transaction submission failed" + } + }.onFailure { error -> + Timber.tag(TAG).e(error, "Failed to build transaction") + submissionState = SubmissionState.Failed("Failed to build transaction") + errorMessage = error.message ?: "Transaction build failed" + } + } + + // Poll for confirmation + val currentTxHash = txHash + LaunchedEffect(currentTxHash) { + if (currentTxHash == null) return@LaunchedEffect + if (submissionState !is SubmissionState.Pending) return@LaunchedEffect + + Timber.tag(TAG).d("Starting to poll for confirmation: $currentTxHash") + + paymentStatusPoller.pollUntilConfirmed(currentTxHash).collect { status -> + txStatus = status + when (status) { + TxStatus.CONFIRMED -> { + Timber.tag(TAG).i("Transaction confirmed: $currentTxHash") + submissionState = SubmissionState.Confirmed + } + TxStatus.FAILED -> { + Timber.tag(TAG).w("Transaction failed: $currentTxHash") + submissionState = SubmissionState.Failed("Transaction failed on chain") + errorMessage = "Transaction was rejected by the network" + } + TxStatus.PENDING -> { + val elapsed = System.currentTimeMillis() - submissionStartTime + if (elapsed > TIMEOUT_THRESHOLD_MS) { + Timber.tag(TAG).w("Transaction taking too long: $currentTxHash") + submissionState = SubmissionState.TakingTooLong + } + } + } + } + } + + // Send Matrix event on confirmation + LaunchedEffect(submissionState, eventSent) { + if (submissionState == SubmissionState.Confirmed && !eventSent) { + val req = lastRequest ?: return@LaunchedEffect + val signedTx = lastSignedTx ?: return@LaunchedEffect + + eventSent = true + + val room = matrixClient.getRoom(roomId) + val joinedRoom = room as? JoinedRoom + val timeline = joinedRoom?.liveTimeline + + if (timeline != null) { + paymentEventSender.sendPaymentEvent( + timeline = timeline, + request = req, + signedTx = signedTx, + network = CardanoNetworkConfig.NETWORK_NAME, + ).onSuccess { + Timber.tag(TAG).i("Payment event sent to timeline") + }.onFailure { e -> + Timber.tag(TAG).e(e, "Failed to send payment event to timeline") + // Non-fatal - tx succeeded, just event didn't send + } + } else { + Timber.tag(TAG).w("Could not get room timeline to send payment event") + } + } + } + + val explorerUrl = txHash?.let { + "${CardanoNetworkConfig.EXPLORER_BASE_URL}/transaction/$it" + } + + // Build display amount + val displayAmount = if (isSendingToken && assetDisplayName != null) { + "$assetQuantity $assetDisplayName" + } else { + "${PaymentConfirmationState.formatAda(amountLovelace)} ADA" + } + + return PaymentProgressState( + txHash = txHash, + txHashDisplay = txHash?.let { PaymentProgressState.truncateTxHash(it) }, + explorerUrl = explorerUrl, + amountLovelace = amountLovelace, + amountAda = PaymentConfirmationState.formatAda(amountLovelace), + displayAmount = displayAmount, + isSendingToken = isSendingToken, + assetDisplayName = assetDisplayName, + assetQuantity = assetQuantity, + recipientAddress = recipientAddress, + txStatus = txStatus, + submissionState = submissionState, + errorMessage = errorMessage, + isTestnet = CardanoNetworkConfig.NETWORK == CardanoNetwork.TESTNET, + eventSink = {}, + ) + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressState.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressState.kt new file mode 100644 index 0000000000..588422bd8c --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressState.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import io.element.android.features.wallet.api.TxStatus +import io.element.android.features.wallet.impl.slash.Lovelace + +/** + * State for the payment progress screen. + */ +data class PaymentProgressState( + val txHash: String?, + val txHashDisplay: String?, + val explorerUrl: String?, + val amountLovelace: Lovelace, + val amountAda: String, + /** Formatted display amount (e.g., "10 ADA" or "1000 HOSKY"). */ + val displayAmount: String, + /** True if sending a native asset (token). */ + val isSendingToken: Boolean, + /** Display name of the token being sent. */ + val assetDisplayName: String?, + /** Quantity of the token being sent. */ + val assetQuantity: Long?, + val recipientAddress: String, + val txStatus: TxStatus, + val submissionState: SubmissionState, + val errorMessage: String?, + val isTestnet: Boolean, + val eventSink: (PaymentFlowEvents) -> Unit, +) { + companion object { + fun truncateTxHash(txHash: String): String { + if (txHash.length <= 20) return txHash + return "${txHash.take(8)}...${txHash.takeLast(6)}" + } + } +} + +/** + * State of the transaction submission and confirmation process. + */ +sealed interface SubmissionState { + data object Submitting : SubmissionState + data object Pending : SubmissionState + data object Confirmed : SubmissionState + data class Failed(val reason: String) : SubmissionState + data object TakingTooLong : SubmissionState +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressView.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressView.kt new file mode 100644 index 0000000000..477dd0d52c --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressView.kt @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.OpenInNew +import androidx.compose.material.icons.filled.Schedule +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import io.element.android.features.wallet.api.TxStatus +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.OutlinedButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PaymentProgressView( + state: PaymentProgressState, + onDone: () -> Unit, + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + Scaffold( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + topBar = { + TopAppBar( + title = { Text("Payment") }, + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(48.dp)) + + // Status icon + when (state.submissionState) { + SubmissionState.Submitting -> { + CircularProgressIndicator( + modifier = Modifier.size(80.dp), + strokeWidth = 4.dp, + ) + } + SubmissionState.Pending -> { + Icon( + imageVector = Icons.Default.Schedule, + contentDescription = "Pending", + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + SubmissionState.Confirmed -> { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = "Confirmed", + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + is SubmissionState.Failed -> { + Icon( + imageVector = Icons.Default.Error, + contentDescription = "Failed", + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.error, + ) + } + SubmissionState.TakingTooLong -> { + Icon( + imageVector = Icons.Default.Schedule, + contentDescription = "Taking longer than expected", + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.tertiary, + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Status title + Text( + text = when (state.submissionState) { + SubmissionState.Submitting -> "Signing & Submitting..." + SubmissionState.Pending -> "Transaction Submitted" + SubmissionState.Confirmed -> "Payment Sent!" + is SubmissionState.Failed -> "Payment Failed" + SubmissionState.TakingTooLong -> "Taking Longer Than Expected" + }, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Status subtitle + Text( + text = when (state.submissionState) { + SubmissionState.Submitting -> "Please wait..." + SubmissionState.Pending -> "Waiting for confirmation..." + SubmissionState.Confirmed -> "${state.displayAmount} sent" + is SubmissionState.Failed -> state.errorMessage ?: "Transaction failed" + SubmissionState.TakingTooLong -> "The network is busy. Your transaction may still confirm." + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + // Transaction hash card (when available) + state.txHash?.let { txHash -> + TransactionHashCard( + txHashDisplay = state.txHashDisplay ?: txHash, + explorerUrl = state.explorerUrl, + onViewOnExplorer = { + state.explorerUrl?.let { url -> + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) + } + }, + ) + } + + // Testnet notice + if (state.isTestnet && state.submissionState == SubmissionState.Confirmed) { + Spacer(modifier = Modifier.height(16.dp)) + TestnetNotice() + } + + Spacer(modifier = Modifier.weight(1f)) + + // Action buttons + when (state.submissionState) { + SubmissionState.Submitting, + SubmissionState.Pending -> { + // No buttons while in progress + } + SubmissionState.Confirmed -> { + Button( + text = "Done", + onClick = { + state.eventSink(PaymentFlowEvents.Done) + onDone() + }, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + ) + } + is SubmissionState.Failed -> { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + OutlinedButton( + text = "Cancel", + onClick = { + state.eventSink(PaymentFlowEvents.Cancel) + onDone() + }, + modifier = Modifier.weight(1f), + ) + Button( + text = "Try Again", + onClick = { + state.eventSink(PaymentFlowEvents.RetryPayment) + onRetry() + }, + modifier = Modifier.weight(1f), + ) + } + } + SubmissionState.TakingTooLong -> { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + state.explorerUrl?.let { url -> + OutlinedButton( + text = "View on Explorer", + onClick = { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + Button( + text = "Done", + onClick = { + state.eventSink(PaymentFlowEvents.Done) + onDone() + }, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + ) + } + } + } + } + } +} + +@Composable +private fun TransactionHashCard( + txHashDisplay: String, + explorerUrl: String?, + onViewOnExplorer: () -> Unit, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = "Transaction ID", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + ) + Text( + text = txHashDisplay, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + ) + if (explorerUrl != null) { + Row( + modifier = Modifier + .clickable(onClick = onViewOnExplorer) + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "View on CardanoScan", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + ) + Icon( + imageVector = Icons.Default.OpenInNew, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + } + } + } +} + +@Composable +private fun TestnetNotice(modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer + ), + ) { + Text( + text = "This was a testnet transaction — no real ADA was transferred.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onTertiaryContainer, + modifier = Modifier.padding(12.dp), + textAlign = TextAlign.Center, + ) + } +} + +// Preview support +@PreviewsDayNight +@Composable +internal fun PaymentProgressViewPreview( + @PreviewParameter(PaymentProgressStateProvider::class) state: PaymentProgressState +) { + ElementPreview { + PaymentProgressView( + state = state, + onDone = {}, + onRetry = {}, + ) + } +} + +internal class PaymentProgressStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + // Submitting + PaymentProgressState( + txHash = null, + txHashDisplay = null, + explorerUrl = null, + amountLovelace = 10_000_000L, + amountAda = "10", + displayAmount = "10 ADA", + isSendingToken = false, + assetDisplayName = null, + assetQuantity = null, + recipientAddress = "addr_test1...", + txStatus = TxStatus.PENDING, + submissionState = SubmissionState.Submitting, + errorMessage = null, + isTestnet = true, + eventSink = {}, + ), + // Pending + PaymentProgressState( + txHash = "abc123def456789012345678901234567890123456789012345678901234", + txHashDisplay = "abc123de...901234", + explorerUrl = "https://preprod.cardanoscan.io/transaction/abc123...", + amountLovelace = 10_000_000L, + amountAda = "10", + displayAmount = "10 ADA", + isSendingToken = false, + assetDisplayName = null, + assetQuantity = null, + recipientAddress = "addr_test1...", + txStatus = TxStatus.PENDING, + submissionState = SubmissionState.Pending, + errorMessage = null, + isTestnet = true, + eventSink = {}, + ), + // Confirmed + PaymentProgressState( + txHash = "abc123def456789012345678901234567890123456789012345678901234", + txHashDisplay = "abc123de...901234", + explorerUrl = "https://preprod.cardanoscan.io/transaction/abc123...", + amountLovelace = 10_000_000L, + amountAda = "10", + displayAmount = "10 ADA", + isSendingToken = false, + assetDisplayName = null, + assetQuantity = null, + recipientAddress = "addr_test1...", + txStatus = TxStatus.CONFIRMED, + submissionState = SubmissionState.Confirmed, + errorMessage = null, + isTestnet = true, + eventSink = {}, + ), + // Failed + PaymentProgressState( + txHash = null, + txHashDisplay = null, + explorerUrl = null, + amountLovelace = 10_000_000L, + amountAda = "10", + displayAmount = "10 ADA", + isSendingToken = false, + assetDisplayName = null, + assetQuantity = null, + recipientAddress = "addr_test1...", + txStatus = TxStatus.FAILED, + submissionState = SubmissionState.Failed("Transaction rejected: insufficient funds"), + errorMessage = "Transaction rejected: insufficient funds", + isTestnet = true, + eventSink = {}, + ), + ) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/seedphrase/SeedPhraseManager.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/seedphrase/SeedPhraseManager.kt new file mode 100644 index 0000000000..43c2041c03 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/seedphrase/SeedPhraseManager.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.seedphrase + +import com.bloxbean.cardano.client.crypto.bip39.MnemonicCode +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import timber.log.Timber +import java.security.SecureRandom + +sealed class SeedPhraseValidationResult { + data class Valid(val wordCount: Int) : SeedPhraseValidationResult() + data class Invalid(val error: String) : SeedPhraseValidationResult() +} + +interface SeedPhraseManager { + fun generateSeedPhrase(): List + fun generateSeedPhrase(wordCount: Int): List + fun validate(words: List): SeedPhraseValidationResult + fun validate(seedPhrase: String): SeedPhraseValidationResult + fun normalize(input: String): List + fun getWordlist(): List + fun suggestWords(prefix: String, limit: Int = 5): List +} + +@ContributesBinding(AppScope::class) +class DefaultSeedPhraseManager @Inject constructor() : SeedPhraseManager { + + companion object { + private const val DEFAULT_WORD_COUNT = 24 + private val VALID_WORD_COUNTS = setOf(12, 15, 18, 21, 24) + private val ENTROPY_BITS_MAP = mapOf( + 12 to 128, + 15 to 160, + 18 to 192, + 21 to 224, + 24 to 256, + ) + } + + private val mnemonicCode = MnemonicCode() + + private val wordList: List by lazy { + mnemonicCode.wordList + } + + override fun generateSeedPhrase(): List { + return generateSeedPhrase(DEFAULT_WORD_COUNT) + } + + override fun generateSeedPhrase(wordCount: Int): List { + require(wordCount in VALID_WORD_COUNTS) { + "Invalid word count: $wordCount. Must be one of: $VALID_WORD_COUNTS" + } + + val entropyBits = ENTROPY_BITS_MAP[wordCount] + ?: throw IllegalStateException("Missing entropy mapping for word count: $wordCount") + + val entropyBytes = entropyBits / 8 + val entropy = ByteArray(entropyBytes) + SecureRandom().nextBytes(entropy) + + val words = try { + mnemonicCode.toMnemonic(entropy) + } finally { + entropy.fill(0) + } + + Timber.d("Generated $wordCount-word seed phrase") + return words + } + + override fun validate(words: List): SeedPhraseValidationResult { + if (words.size !in VALID_WORD_COUNTS) { + return SeedPhraseValidationResult.Invalid( + "Invalid word count: ${words.size}. Expected one of: $VALID_WORD_COUNTS" + ) + } + + val invalidWords = words.filter { it.lowercase() !in wordList } + if (invalidWords.isNotEmpty()) { + return SeedPhraseValidationResult.Invalid( + "Invalid words: ${invalidWords.joinToString(", ")}" + ) + } + + return try { + mnemonicCode.check(words.map { it.lowercase() }) + SeedPhraseValidationResult.Valid(words.size) + } catch (e: Exception) { + SeedPhraseValidationResult.Invalid("Invalid checksum: ${e.message}") + } + } + + override fun validate(seedPhrase: String): SeedPhraseValidationResult { + val words = normalize(seedPhrase) + return validate(words) + } + + override fun normalize(input: String): List { + return input + .trim() + .lowercase() + .split(Regex("\\s+")) + .filter { it.isNotBlank() } + } + + override fun getWordlist(): List { + return wordList + } + + override fun suggestWords(prefix: String, limit: Int): List { + if (prefix.isBlank()) { + return emptyList() + } + + val normalizedPrefix = prefix.trim().lowercase() + return wordList + .filter { it.startsWith(normalizedPrefix) } + .take(limit) + } +} diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationNode.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupNode.kt similarity index 55% rename from features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationNode.kt rename to features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupNode.kt index a8db4d2d75..93d503d9ad 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationNode.kt +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupNode.kt @@ -1,11 +1,10 @@ /* - * Copyright (c) 2026 Element Creations Ltd. + * Copyright (c) 2026 Sulkta Coop. * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. + * SPDX-License-Identifier: AGPL-3.0-only */ -package io.element.android.features.linknewdevice.impl.screens.confirmation +package io.element.android.features.wallet.impl.setup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -15,33 +14,32 @@ 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( +class WalletSetupNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val presenter: WalletSetupPresenter, ) : Node(buildContext = buildContext, plugins = plugins) { + interface Callback : Plugin { - fun onCancel() + fun onSetupComplete() + fun onBack() } - 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, + val state = presenter.present() + + WalletSetupView( + state = state, + onComplete = { callback.onSetupComplete() }, + onBack = { callback.onBack() }, + modifier = modifier, ) } } diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupPresenter.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupPresenter.kt new file mode 100644 index 0000000000..4afff5b99f --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupPresenter.kt @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.setup + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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 dev.zacsweers.metro.Inject +import io.element.android.features.wallet.api.address.CardanoAddressService +import io.element.android.features.wallet.api.backup.WalletBackupService +import io.element.android.features.wallet.api.storage.CardanoKeyStorage +import io.element.android.features.wallet.impl.cardano.CardanoWalletManager +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import kotlinx.coroutines.launch +import timber.log.Timber + +class WalletSetupPresenter @Inject constructor( + private val keyStorage: CardanoKeyStorage, + private val walletManager: CardanoWalletManager, + private val matrixClient: MatrixClient, + private val walletBackupService: WalletBackupService, + private val cardanoAddressService: CardanoAddressService, +) : Presenter { + + companion object { + private const val TAG = "WalletSetupPresenter" + private val VALID_WORD_COUNTS = listOf(12, 15, 18, 21, 24) + } + + /** + * Publish the Cardano address to Matrix account data for discovery. + * This is fire-and-forget - we don't fail the wallet setup if publishing fails. + */ + private suspend fun publishAddressToMatrix(address: String) { + cardanoAddressService.publishAddress(address) + .onSuccess { + Timber.tag(TAG).i("Published Cardano address to Matrix account data") + } + .onFailure { e -> + Timber.tag(TAG).w(e, "Failed to publish Cardano address (non-fatal)") + } + } + + @Composable + override fun present(): WalletSetupState { + val scope = rememberCoroutineScope() + val sessionId = matrixClient.sessionId + + var step by remember { mutableStateOf(SetupStep.WELCOME) } + var generatedMnemonic by remember { mutableStateOf>(emptyList()) } + var generatedAddress by remember { mutableStateOf(null) } + var isGenerating by remember { mutableStateOf(false) } + var error by remember { mutableStateOf(null) } + var hasConfirmedBackup by remember { mutableStateOf(false) } + var isBackingUp by remember { mutableStateOf(false) } + var recoveryKeyInput by remember { mutableStateOf("") } + // Import state + var importMnemonicInput by remember { mutableStateOf("") } + var importWordCount by remember { mutableIntStateOf(0) } + var isImporting by remember { mutableStateOf(false) } + // Cloud backup state + var hasCloudBackup by remember { mutableStateOf(false) } + var isCheckingCloudBackup by remember { mutableStateOf(true) } + var cloudRestoreRecoveryKey by remember { mutableStateOf("") } + var isRestoringFromCloud by remember { mutableStateOf(false) } + + // Check for cloud backup on init + LaunchedEffect(Unit) { + Timber.tag(TAG).d("Checking for cloud backup...") + walletBackupService.hasBackupWithoutKey() + .onSuccess { exists -> + Timber.tag(TAG).d("Cloud backup exists: $exists") + hasCloudBackup = exists + } + .onFailure { e -> + Timber.tag(TAG).w(e, "Failed to check for cloud backup") + hasCloudBackup = false + } + isCheckingCloudBackup = false + } + + fun handleEvent(event: WalletSetupEvent) { + when (event) { + WalletSetupEvent.CreateNewWallet -> { + step = SetupStep.GENERATING + isGenerating = true + error = null + + scope.launch { + keyStorage.generateWallet(sessionId) + .onSuccess { result -> + Timber.tag(TAG).i("Wallet generated: ${result.baseAddress.take(20)}...") + generatedMnemonic = result.mnemonic + generatedAddress = result.baseAddress + isGenerating = false + step = SetupStep.SHOW_ADDRESS + } + .onFailure { e -> + Timber.tag(TAG).e(e, "Failed to generate wallet") + error = e.message ?: "Failed to generate wallet" + isGenerating = false + step = SetupStep.WELCOME + } + } + } + + WalletSetupEvent.ImportExistingWallet -> { + step = SetupStep.IMPORT_MNEMONIC + importMnemonicInput = "" + importWordCount = 0 + error = null + } + + WalletSetupEvent.RestoreFromCloud -> { + step = SetupStep.RESTORE_FROM_CLOUD + cloudRestoreRecoveryKey = "" + error = null + } + + is WalletSetupEvent.UpdateImportMnemonic -> { + importMnemonicInput = event.text + // Count words (split by whitespace) + val words = event.text.trim().split(Regex("\\s+")).filter { it.isNotEmpty() } + importWordCount = words.size + // Clear error on input change + error = null + } + + WalletSetupEvent.ClearImportMnemonic -> { + importMnemonicInput = "" + importWordCount = 0 + error = null + } + + WalletSetupEvent.ConfirmImport -> { + val words = importMnemonicInput.trim().lowercase().split(Regex("\\s+")).filter { it.isNotEmpty() } + + if (words.size !in VALID_WORD_COUNTS) { + error = "Invalid recovery phrase. Expected 12 or 24 words, got ${words.size}." + return + } + + isImporting = true + error = null + + scope.launch { + keyStorage.importWallet(sessionId, words) + .onSuccess { address -> + Timber.tag(TAG).i("Wallet imported: ${address.take(20)}...") + generatedMnemonic = words + generatedAddress = address + isImporting = false + // Publish address to Matrix for discovery + publishAddressToMatrix(address) + // Skip to address confirmation (no backup prompt for imported wallets + // since user already has their phrase) + step = SetupStep.SHOW_ADDRESS + } + .onFailure { e -> + Timber.tag(TAG).e(e, "Failed to import wallet") + error = when { + e.message?.contains("invalid", ignoreCase = true) == true -> + "Invalid recovery phrase. Check your words and try again." + e.message?.contains("already exists", ignoreCase = true) == true -> + "A wallet already exists for this account." + else -> e.message ?: "Failed to import wallet" + } + isImporting = false + } + } + } + + is WalletSetupEvent.UpdateCloudRestoreRecoveryKey -> { + cloudRestoreRecoveryKey = event.key + error = null + } + + WalletSetupEvent.ConfirmCloudRestore -> { + if (cloudRestoreRecoveryKey.isBlank()) { + error = "Please enter your Matrix recovery key" + return + } + + isRestoringFromCloud = true + error = null + + scope.launch { + // Normalize recovery key: remove spaces and convert to lowercase + val normalizedKey = cloudRestoreRecoveryKey.replace("\\s+".toRegex(), "").lowercase() + + walletBackupService.restoreSeed(normalizedKey) + .onSuccess { mnemonic -> + if (mnemonic != null) { + Timber.tag(TAG).i("Restored mnemonic from SSSS: ${mnemonic.size} words") + + // Import the restored mnemonic + keyStorage.importWallet(sessionId, mnemonic) + .onSuccess { address -> + Timber.tag(TAG).i("Wallet restored from cloud: ${address.take(20)}...") + generatedMnemonic = mnemonic + generatedAddress = address + isRestoringFromCloud = false + // Publish address to Matrix for discovery + publishAddressToMatrix(address) + // Go directly to address confirmation + step = SetupStep.SHOW_ADDRESS + } + .onFailure { e -> + Timber.tag(TAG).e(e, "Failed to import restored wallet") + error = e.message ?: "Failed to import restored wallet" + isRestoringFromCloud = false + } + } else { + error = "No wallet backup found in Matrix" + isRestoringFromCloud = false + } + } + .onFailure { e -> + Timber.tag(TAG).e(e, "Failed to restore from cloud") + error = when { + e.message?.contains("invalid", ignoreCase = true) == true -> + "Invalid recovery key. Please check and try again." + e.message?.contains("not set up", ignoreCase = true) == true -> + "Matrix recovery is not set up for this account." + else -> e.message ?: "Failed to restore from Matrix" + } + isRestoringFromCloud = false + } + } + } + + WalletSetupEvent.ProceedToBackup -> { + step = SetupStep.BACKUP_PROMPT + } + + WalletSetupEvent.ProceedToMatrixBackup -> { + step = SetupStep.BACKUP_TO_MATRIX + recoveryKeyInput = "" + } + + WalletSetupEvent.SkipBackupToMatrix -> { + hasConfirmedBackup = true + step = SetupStep.COMPLETE + scope.launch { + // Publish address to Matrix for discovery + generatedAddress?.let { publishAddressToMatrix(it) } + walletManager.initialize(sessionId) + } + } + + is WalletSetupEvent.UpdateRecoveryKeyInput -> { + recoveryKeyInput = event.key + } + + WalletSetupEvent.ConfirmMatrixBackup -> { + if (recoveryKeyInput.isBlank()) { + error = "Please enter your Matrix recovery key" + return + } + + isBackingUp = true + error = null + + scope.launch { + walletBackupService.backupSeed(recoveryKeyInput, generatedMnemonic) + .onSuccess { + Timber.tag(TAG).i("Wallet backed up to SSSS") + isBackingUp = false + hasConfirmedBackup = true + step = SetupStep.COMPLETE + // Publish address to Matrix for discovery + generatedAddress?.let { publishAddressToMatrix(it) } + walletManager.initialize(sessionId) + } + .onFailure { e -> + Timber.tag(TAG).e(e, "Failed to backup wallet") + error = when { + e.message?.contains("invalid", ignoreCase = true) == true -> + "Invalid recovery key. Please check and try again." + e.message?.contains("not set up", ignoreCase = true) == true -> + "Matrix recovery is not set up. Please set up Security & Privacy first." + else -> e.message ?: "Backup failed" + } + isBackingUp = false + } + } + } + + WalletSetupEvent.ConfirmBackup -> { + hasConfirmedBackup = true + step = SetupStep.COMPLETE + scope.launch { + // Publish address to Matrix for discovery + generatedAddress?.let { publishAddressToMatrix(it) } + walletManager.initialize(sessionId) + } + } + + WalletSetupEvent.Complete -> { + // Callback handled by node + } + + WalletSetupEvent.Back -> { + when (step) { + SetupStep.IMPORT_MNEMONIC -> step = SetupStep.WELCOME + SetupStep.RESTORE_FROM_CLOUD -> step = SetupStep.WELCOME + SetupStep.SHOW_ADDRESS -> step = SetupStep.WELCOME + SetupStep.BACKUP_PROMPT -> step = SetupStep.SHOW_ADDRESS + SetupStep.BACKUP_TO_MATRIX -> step = SetupStep.BACKUP_PROMPT + else -> { /* Let node handle close */ } + } + } + + WalletSetupEvent.DismissError -> { + error = null + } + } + } + + return WalletSetupState( + step = step, + generatedMnemonic = generatedMnemonic, + generatedAddress = generatedAddress, + isGenerating = isGenerating, + error = error, + hasConfirmedBackup = hasConfirmedBackup, + isBackingUp = isBackingUp, + recoveryKeyInput = recoveryKeyInput, + importMnemonicInput = importMnemonicInput, + importWordCount = importWordCount, + isImporting = isImporting, + hasCloudBackup = hasCloudBackup, + isCheckingCloudBackup = isCheckingCloudBackup, + cloudRestoreRecoveryKey = cloudRestoreRecoveryKey, + isRestoringFromCloud = isRestoringFromCloud, + eventSink = ::handleEvent, + ) + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupState.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupState.kt new file mode 100644 index 0000000000..23ad6860bf --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupState.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.setup + +import androidx.compose.runtime.Immutable + +/** + * UI state for wallet setup flow. + */ +@Immutable +data class WalletSetupState( + val step: SetupStep, + val generatedMnemonic: List, + val generatedAddress: String?, + val isGenerating: Boolean, + val error: String?, + val hasConfirmedBackup: Boolean, + val isBackingUp: Boolean, + val recoveryKeyInput: String, + val importMnemonicInput: String, + val importWordCount: Int, + val isImporting: Boolean, + val hasCloudBackup: Boolean, + val isCheckingCloudBackup: Boolean, + val cloudRestoreRecoveryKey: String, + val isRestoringFromCloud: Boolean, + val eventSink: (WalletSetupEvent) -> Unit, +) + +/** + * Steps in the wallet setup flow. + */ +enum class SetupStep { + /** Initial screen with Create/Import/Restore options. */ + WELCOME, + /** Generating wallet keys. */ + GENERATING, + /** Display the generated address. */ + SHOW_ADDRESS, + /** Prompt to backup recovery phrase. */ + BACKUP_PROMPT, + /** Backup to Matrix SSSS. */ + BACKUP_TO_MATRIX, + /** Setup complete. */ + COMPLETE, + /** Import existing wallet by entering mnemonic. */ + IMPORT_MNEMONIC, + /** Restore from Matrix cloud backup - enter recovery key. */ + RESTORE_FROM_CLOUD, +} + +/** + * Events that can be triggered from the wallet setup UI. + */ +sealed interface WalletSetupEvent { + /** User wants to create a new wallet. */ + data object CreateNewWallet : WalletSetupEvent + + /** User wants to import an existing wallet. */ + data object ImportExistingWallet : WalletSetupEvent + + /** User wants to restore from Matrix cloud backup. */ + data object RestoreFromCloud : WalletSetupEvent + + /** Update the import mnemonic text. */ + data class UpdateImportMnemonic(val text: String) : WalletSetupEvent + + /** Clear the import mnemonic input. */ + data object ClearImportMnemonic : WalletSetupEvent + + /** Confirm import of the entered mnemonic. */ + data object ConfirmImport : WalletSetupEvent + + /** Proceed from address display to backup prompt. */ + data object ProceedToBackup : WalletSetupEvent + + /** User wants to backup to Matrix SSSS. */ + data object ProceedToMatrixBackup : WalletSetupEvent + + /** User chose to skip Matrix backup. */ + data object SkipBackupToMatrix : WalletSetupEvent + + /** Update the recovery key input for Matrix backup. */ + data class UpdateRecoveryKeyInput(val key: String) : WalletSetupEvent + + /** Confirm Matrix backup with the entered recovery key. */ + data object ConfirmMatrixBackup : WalletSetupEvent + + /** User confirmed they've backed up their phrase. */ + data object ConfirmBackup : WalletSetupEvent + + /** Setup flow is complete. */ + data object Complete : WalletSetupEvent + + /** Navigate back within the flow. */ + data object Back : WalletSetupEvent + + /** Dismiss any error dialog. */ + data object DismissError : WalletSetupEvent + + /** Update the cloud restore recovery key input. */ + data class UpdateCloudRestoreRecoveryKey(val key: String) : WalletSetupEvent + + /** Confirm cloud restore with the entered recovery key. */ + data object ConfirmCloudRestore : WalletSetupEvent +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupView.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupView.kt new file mode 100644 index 0000000000..0861633c61 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/setup/WalletSetupView.kt @@ -0,0 +1,738 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.setup + +import android.view.WindowManager +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Cloud +import androidx.compose.material.icons.filled.CloudSync +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Key +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +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 +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.OutlinedButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WalletSetupView( + state: WalletSetupState, + onComplete: () -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + // FLAG_SECURE when showing sensitive data + val view = LocalView.current + DisposableEffect(state.step) { + val sensitiveSteps = listOf( + SetupStep.BACKUP_PROMPT, + SetupStep.BACKUP_TO_MATRIX, + SetupStep.IMPORT_MNEMONIC + ) + if (state.step in sensitiveSteps) { + val window = (view.context as? android.app.Activity)?.window + window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + onDispose { window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } + } else { + onDispose { } + } + } + + val title = when (state.step) { + SetupStep.IMPORT_MNEMONIC -> "Import Wallet" + SetupStep.RESTORE_FROM_CLOUD -> "Restore from Matrix" + else -> "Set Up Wallet" + } + + Scaffold( + modifier = modifier.fillMaxSize().systemBarsPadding(), + topBar = { + TopAppBar( + title = { Text(title) }, + navigationIcon = { + if (state.step != SetupStep.COMPLETE) { + IconButton(onClick = { + if (state.step == SetupStep.WELCOME) { + onBack() + } else { + state.eventSink(WalletSetupEvent.Back) + } + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + } + } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (state.step) { + SetupStep.WELCOME -> WelcomeContent(state) + SetupStep.GENERATING -> GeneratingContent() + SetupStep.IMPORT_MNEMONIC -> ImportMnemonicContent(state) + SetupStep.RESTORE_FROM_CLOUD -> RestoreFromCloudContent(state) + SetupStep.SHOW_ADDRESS -> AddressContent(state) + SetupStep.BACKUP_PROMPT -> BackupContent(state) + SetupStep.BACKUP_TO_MATRIX -> MatrixBackupContent(state) + SetupStep.COMPLETE -> CompleteContent(onComplete) + } + } + } +} + +@Composable +private fun ColumnScope.WelcomeContent(state: WalletSetupState) { + Spacer(modifier = Modifier.height(48.dp)) + + Text( + text = "Create Your Wallet", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Your Cardano wallet will be secured with your device's biometric authentication.", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Button( + text = "Create New Wallet", + onClick = { state.eventSink(WalletSetupEvent.CreateNewWallet) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = IconSource.Vector(Icons.Default.Add), + ) + + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedButton( + text = "Import Existing Wallet", + onClick = { state.eventSink(WalletSetupEvent.ImportExistingWallet) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = IconSource.Vector(Icons.Default.Download), + ) + + // Show "Restore from Matrix Backup" if cloud backup exists + if (state.hasCloudBackup && !state.isCheckingCloudBackup) { + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedButton( + text = "Restore from Matrix Backup", + onClick = { state.eventSink(WalletSetupEvent.RestoreFromCloud) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = IconSource.Vector(Icons.Default.CloudSync), + ) + } + + // Show loading indicator while checking for cloud backup + if (state.isCheckingCloudBackup) { + Spacer(modifier = Modifier.height(12.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth(), + ) { + CircularProgressIndicator(modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = "Checking for cloud backup...", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + state.error?.let { error -> + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer), + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = error, + modifier = Modifier.padding(16.dp), + color = MaterialTheme.colorScheme.onErrorContainer, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +private fun ColumnScope.GeneratingContent() { + Spacer(modifier = Modifier.weight(1f)) + + CircularProgressIndicator(modifier = Modifier.size(64.dp)) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Generating Wallet...", + style = MaterialTheme.typography.titleLarge, + ) + + Text( + text = "Creating secure keys", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.weight(1f)) +} + +@Composable +private fun ColumnScope.ImportMnemonicContent(state: WalletSetupState) { + val isValidWordCount = state.importWordCount in listOf(12, 24) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Icon( + imageVector = Icons.Default.Download, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.primary, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Import Existing Wallet", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Enter your 12 or 24-word recovery phrase, separated by spaces.", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + OutlinedTextField( + value = state.importMnemonicInput, + onValueChange = { state.eventSink(WalletSetupEvent.UpdateImportMnemonic(it)) }, + label = { Text("Recovery Phrase") }, + placeholder = { Text("word1 word2 word3 ...") }, + modifier = Modifier.fillMaxWidth(), + minLines = 4, + maxLines = 6, + enabled = !state.isImporting, + trailingIcon = { + if (state.importMnemonicInput.isNotEmpty()) { + IconButton(onClick = { state.eventSink(WalletSetupEvent.ClearImportMnemonic) }) { + Icon(Icons.Default.Clear, contentDescription = "Clear") + } + } + }, + supportingText = { + val color = when { + state.importWordCount == 0 -> MaterialTheme.colorScheme.onSurfaceVariant + isValidWordCount -> MaterialTheme.colorScheme.primary + else -> MaterialTheme.colorScheme.error + } + Text( + text = "${state.importWordCount}/24 words", + color = color, + ) + }, + isError = state.error != null, + ) + + state.error?.let { error -> + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = error, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + if (state.isImporting) { + CircularProgressIndicator(modifier = Modifier.size(32.dp)) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Verifying and importing...", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + Button( + text = "Restore Wallet", + onClick = { state.eventSink(WalletSetupEvent.ConfirmImport) }, + enabled = isValidWordCount, + modifier = Modifier.fillMaxWidth(), + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@Composable +private fun ColumnScope.RestoreFromCloudContent(state: WalletSetupState) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(24.dp)) + + Icon( + imageVector = Icons.Default.CloudSync, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.primary, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Restore from Matrix", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Your wallet backup was found in your Matrix account. Enter your recovery key to restore it.", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) + ), + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = "Enter the same Matrix recovery key you used when setting up Security & Privacy.", + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + OutlinedTextField( + value = state.cloudRestoreRecoveryKey, + onValueChange = { state.eventSink(WalletSetupEvent.UpdateCloudRestoreRecoveryKey(it)) }, + label = { Text("Recovery Key") }, + placeholder = { Text("AAAA BBBB CCCC ...") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + enabled = !state.isRestoringFromCloud, + isError = state.error != null, + ) + + state.error?.let { error -> + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = error, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + if (state.isRestoringFromCloud) { + CircularProgressIndicator(modifier = Modifier.size(32.dp)) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Decrypting and restoring...", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + Button( + text = "Restore Wallet", + onClick = { state.eventSink(WalletSetupEvent.ConfirmCloudRestore) }, + enabled = state.cloudRestoreRecoveryKey.isNotBlank(), + modifier = Modifier.fillMaxWidth(), + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@Composable +private fun ColumnScope.AddressContent(state: WalletSetupState) { + Spacer(modifier = Modifier.height(48.dp)) + + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.primary, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Wallet Ready!", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Your Cardano Address:", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = state.generatedAddress ?: "", + modifier = Modifier.padding(16.dp), + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.bodySmall, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + // For imported wallets, go directly to complete + // For generated wallets, show backup prompt + if (state.generatedMnemonic.size == 24 && state.step == SetupStep.SHOW_ADDRESS) { + Button( + text = "Continue to Backup", + onClick = { state.eventSink(WalletSetupEvent.ProceedToBackup) }, + modifier = Modifier.fillMaxWidth(), + ) + } else { + Button( + text = "Done", + onClick = { + state.eventSink(WalletSetupEvent.ConfirmBackup) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + + Spacer(modifier = Modifier.height(32.dp)) +} + +@Composable +private fun ColumnScope.BackupContent(state: WalletSetupState) { + var isChecked by remember { mutableStateOf(false) } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Back Up Your Wallet", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f) + ), + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = "⚠️ Write down these 24 words in order. Anyone with this phrase can access your funds.", + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + LazyVerticalGrid( + columns = GridCells.Fixed(3), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.weight(1f), + ) { + itemsIndexed(state.generatedMnemonic) { index, word -> + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + ) { + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "${index + 1}.", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = word, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + modifier = Modifier.padding(start = 4.dp), + ) + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Checkbox( + checked = isChecked, + onCheckedChange = { isChecked = it }, + ) + Text( + text = "I have written down my recovery phrase", + style = MaterialTheme.typography.bodyMedium, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Matrix SSSS backup option + Button( + text = "Backup to Matrix", + onClick = { state.eventSink(WalletSetupEvent.ProceedToMatrixBackup) }, + enabled = isChecked, + modifier = Modifier.fillMaxWidth(), + leadingIcon = IconSource.Vector(Icons.Default.Cloud), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedButton( + text = "Skip Cloud Backup", + onClick = { state.eventSink(WalletSetupEvent.SkipBackupToMatrix) }, + enabled = isChecked, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(32.dp)) +} + +@Composable +private fun ColumnScope.MatrixBackupContent(state: WalletSetupState) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(24.dp)) + + Icon( + imageVector = Icons.Default.Key, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.primary, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Backup to Matrix", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Your wallet seed will be encrypted and stored securely in your Matrix account.", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) + ), + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = "Enter your Matrix recovery key (the 48-character key you saved when setting up Security).", + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + OutlinedTextField( + value = state.recoveryKeyInput, + onValueChange = { state.eventSink(WalletSetupEvent.UpdateRecoveryKeyInput(it)) }, + label = { Text("Recovery Key") }, + placeholder = { Text("AAAA BBBB CCCC ...") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + enabled = !state.isBackingUp, + ) + + state.error?.let { error -> + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = error, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + if (state.isBackingUp) { + CircularProgressIndicator(modifier = Modifier.size(32.dp)) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Encrypting and uploading...", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + Button( + text = "Backup Now", + onClick = { state.eventSink(WalletSetupEvent.ConfirmMatrixBackup) }, + enabled = state.recoveryKeyInput.isNotBlank(), + modifier = Modifier.fillMaxWidth(), + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@Composable +private fun ColumnScope.CompleteContent(onComplete: () -> Unit) { + Spacer(modifier = Modifier.weight(1f)) + + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.primary, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "You're All Set!", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Your wallet is ready to use.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Button( + text = "Done", + onClick = onComplete, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(32.dp)) +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/slash/ParsedPayCommand.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/slash/ParsedPayCommand.kt new file mode 100644 index 0000000000..a3081d69a7 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/slash/ParsedPayCommand.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.slash + +import android.os.Parcelable +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.parcelize.Parcelize + +/** + * Lovelace type alias for clarity. + * 1 ADA = 1,000,000 Lovelace + */ +typealias Lovelace = Long + +/** + * Represents the result of parsing a /pay slash command. + * + * Supported input patterns: + * - `/pay 10 ADA addr1xyz...` — pay to explicit Cardano address + * - `/pay 10 ADA @jacob:sulkta.com` — pay to Matrix user + * - `/pay 10 ADA` — pay with no recipient (prompt in payment flow) + * - `/pay 10` — assume ADA unit + * - `/pay 10 tADA` — testnet ADA + * - `/pay` — open payment flow with empty state + */ +sealed interface ParsedPayCommand : Parcelable { + /** + * Payment to an explicit Cardano address. + */ + @Parcelize + data class WithAddressRecipient( + val amount: Lovelace, + val address: String, + val isTestnet: Boolean = false, + ) : ParsedPayCommand + + /** + * Payment to a Matrix user (requires address lookup or manual entry). + */ + @Parcelize + data class WithMatrixRecipient( + val amount: Lovelace, + val matrixUserId: UserId, + val isTestnet: Boolean = false, + ) : ParsedPayCommand + + /** + * Payment with amount only, recipient to be determined in payment flow. + */ + @Parcelize + data class AmountOnly( + val amount: Lovelace, + val isTestnet: Boolean = false, + ) : ParsedPayCommand + + /** + * Empty /pay command - open payment flow with no prefilled data. + */ + @Parcelize + data object Empty : ParsedPayCommand + + /** + * Parse error with a human-readable reason. + */ + @Parcelize + data class ParseError(val reason: String) : ParsedPayCommand +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/slash/SlashCommandParser.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/slash/SlashCommandParser.kt new file mode 100644 index 0000000000..a457fb6f36 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/slash/SlashCommandParser.kt @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.slash + +import io.element.android.libraries.matrix.api.core.UserId +import dev.zacsweers.metro.Inject +import java.math.BigDecimal + +/** + * Parser for /pay slash commands. + * + * Handles various input formats: + * - `/pay` → Empty (open payment flow) + * - `/pay 10` → AmountOnly (assume ADA) + * - `/pay 10 ADA` → AmountOnly + * - `/pay 10 tADA` → AmountOnly (testnet) + * - `/pay 10 ADA @user:server` → WithMatrixRecipient + * - `/pay 10 ADA addr1...` → WithAddressRecipient + */ +@Inject +class SlashCommandParser { + companion object { + private const val MAX_ADA_SUPPLY = 45_000_000_000L // 45 billion ADA + private const val LOVELACE_PER_ADA = 1_000_000L + private const val MIN_CARDANO_ADDRESS_LENGTH = 50 + private const val MAX_CARDANO_ADDRESS_LENGTH = 120 + + // Regex patterns + private val WHITESPACE_REGEX = "\\s+".toRegex() + private val AMOUNT_REGEX = "^\\d+(\\.\\d+)?$".toRegex() + private val MAINNET_ADDRESS_REGEX = "^addr1[a-zA-Z0-9]+$".toRegex() + private val TESTNET_ADDRESS_REGEX = "^addr_test1[a-zA-Z0-9]+$".toRegex() + private val MATRIX_USER_REGEX = "^@[a-zA-Z0-9._=-]+:[a-zA-Z0-9.-]+$".toRegex() + } + + /** + * Parse a message text to see if it's a /pay command. + * + * @param input The raw message text + * @return ParsedPayCommand result, or null if not a /pay command + */ + fun parse(input: String): ParsedPayCommand? { + val trimmed = input.trim() + + // Check if this is a /pay command + if (!trimmed.startsWith("/pay", ignoreCase = true)) { + return null + } + + // Remove the /pay prefix and split remaining tokens + val afterPay = trimmed.substring(4).trim() + + // Empty /pay command + if (afterPay.isEmpty()) { + return ParsedPayCommand.Empty + } + + // Split into tokens + val tokens = afterPay.split(WHITESPACE_REGEX).filter { it.isNotEmpty() } + + return parseTokens(tokens) + } + + /** + * Check if input text looks like a partial /pay command (for suggestion filtering). + */ + fun isPartialPayCommand(input: String): Boolean { + val trimmed = input.trim().lowercase() + if (trimmed.isEmpty()) return false + return "/pay".startsWith(trimmed) || trimmed.startsWith("/pay") + } + + private fun parseTokens(tokens: List): ParsedPayCommand { + if (tokens.isEmpty()) { + return ParsedPayCommand.Empty + } + + // First token should be amount + val amountStr = tokens[0] + val amount = parseAmount(amountStr) + ?: return ParsedPayCommand.ParseError("Invalid amount: '$amountStr'. Expected a number like '10' or '10.5'") + + // Validate amount is positive + if (amount <= 0) { + return ParsedPayCommand.ParseError("Amount must be greater than zero") + } + + // Check for reasonable max (total ADA supply) + if (amount > MAX_ADA_SUPPLY * LOVELACE_PER_ADA) { + return ParsedPayCommand.ParseError("Amount exceeds maximum possible ADA supply (45 billion ADA)") + } + + // If only amount, assume ADA + if (tokens.size == 1) { + return ParsedPayCommand.AmountOnly(amount = amount, isTestnet = false) + } + + // Second token could be unit or recipient + val secondToken = tokens[1] + + // Check if it's a unit specifier + val (lovelaceAmount, isTestnet) = when (secondToken.uppercase()) { + "ADA" -> amount to false + "TADA", "TADA" -> amount to true + "LOVELACE" -> { + // Amount is already in lovelace, convert back to check + val adaEquivalent = amount / LOVELACE_PER_ADA + if (adaEquivalent > MAX_ADA_SUPPLY) { + return ParsedPayCommand.ParseError("Amount exceeds maximum possible ADA supply") + } + amount to false + } + else -> { + // Second token is not a unit - could be recipient + // Assume ADA and treat second token as recipient + return parseWithRecipient(amount, false, secondToken) + } + } + + // If we have unit but no recipient + if (tokens.size == 2) { + return ParsedPayCommand.AmountOnly(amount = lovelaceAmount, isTestnet = isTestnet) + } + + // Third token should be recipient + val recipientToken = tokens[2] + + // Check for extra tokens (shouldn't have more than 3) + if (tokens.size > 3) { + // Allow addresses with accidental spaces? No, be strict. + return ParsedPayCommand.ParseError( + "Too many arguments. Format: /pay [ADA|tADA] [@user:server or addr1...]" + ) + } + + return parseWithRecipient(lovelaceAmount, isTestnet, recipientToken) + } + + private fun parseAmount(amountStr: String): Lovelace? { + if (!AMOUNT_REGEX.matches(amountStr)) { + return null + } + + return try { + val decimal = BigDecimal(amountStr) + + // Check for too many decimal places (max 6 for lovelace precision) + val scale = decimal.scale() + if (scale > 6) { + return null + } + + // Convert to lovelace (multiply by 1,000,000) + val lovelace = decimal.multiply(BigDecimal(LOVELACE_PER_ADA)) + + // Ensure it's a whole number of lovelace + if (lovelace.stripTrailingZeros().scale() > 0) { + return null + } + + lovelace.toLong() + } catch (e: NumberFormatException) { + null + } catch (e: ArithmeticException) { + null + } + } + + private fun parseWithRecipient( + amount: Lovelace, + isTestnet: Boolean, + recipientToken: String, + ): ParsedPayCommand { + // Check for Matrix user ID + if (recipientToken.startsWith("@")) { + if (!MATRIX_USER_REGEX.matches(recipientToken)) { + return ParsedPayCommand.ParseError( + "Invalid Matrix user ID: '$recipientToken'. Expected format: @user:server.com" + ) + } + return try { + val userId = UserId(recipientToken) + ParsedPayCommand.WithMatrixRecipient( + amount = amount, + matrixUserId = userId, + isTestnet = isTestnet, + ) + } catch (e: Exception) { + ParsedPayCommand.ParseError("Invalid Matrix user ID: '$recipientToken'") + } + } + + // Check for Cardano address + return validateAndCreateAddressRecipient(amount, isTestnet, recipientToken) + } + + private fun validateAndCreateAddressRecipient( + amount: Lovelace, + isTestnet: Boolean, + address: String, + ): ParsedPayCommand { + // Check address prefix + val isMainnetAddress = address.startsWith("addr1", ignoreCase = true) + val isTestnetAddress = address.startsWith("addr_test1", ignoreCase = true) + + if (!isMainnetAddress && !isTestnetAddress) { + return ParsedPayCommand.ParseError( + "Invalid Cardano address: must start with 'addr1' (mainnet) or 'addr_test1' (testnet)" + ) + } + + // Validate address length + if (address.length < MIN_CARDANO_ADDRESS_LENGTH) { + return ParsedPayCommand.ParseError( + "Invalid Cardano address: too short (minimum $MIN_CARDANO_ADDRESS_LENGTH characters)" + ) + } + + if (address.length > MAX_CARDANO_ADDRESS_LENGTH) { + return ParsedPayCommand.ParseError( + "Invalid Cardano address: too long (maximum $MAX_CARDANO_ADDRESS_LENGTH characters)" + ) + } + + // Check for valid characters (Bech32) + val addressToCheck = if (isMainnetAddress) { + if (!MAINNET_ADDRESS_REGEX.matches(address)) { + return ParsedPayCommand.ParseError( + "Invalid Cardano address: contains invalid characters" + ) + } + address + } else { + if (!TESTNET_ADDRESS_REGEX.matches(address)) { + return ParsedPayCommand.ParseError( + "Invalid Cardano address: contains invalid characters" + ) + } + address + } + + // Warn about network mismatch + if (isTestnet && isMainnetAddress) { + return ParsedPayCommand.ParseError( + "Network mismatch: using tADA (testnet) but address is mainnet (addr1...)" + ) + } + + if (!isTestnet && isTestnetAddress) { + return ParsedPayCommand.ParseError( + "Network mismatch: using ADA (mainnet) but address is testnet (addr_test1...)" + ) + } + + return ParsedPayCommand.WithAddressRecipient( + amount = amount, + address = addressToCheck, + isTestnet = isTestnet, + ) + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/storage/CardanoKeyStorageImpl.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/storage/CardanoKeyStorageImpl.kt new file mode 100644 index 0000000000..8537374a1f --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/storage/CardanoKeyStorageImpl.kt @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.storage + +import android.content.Context +import android.content.SharedPreferences +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyPermanentlyInvalidatedException +import android.security.keystore.KeyProperties +import android.util.Base64 +import com.bloxbean.cardano.client.account.Account +import com.bloxbean.cardano.client.crypto.bip39.MnemonicCode +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.features.wallet.api.storage.CardanoKeyStorage +import io.element.android.features.wallet.api.storage.WalletCreationResult +import io.element.android.features.wallet.impl.cardano.CardanoNetworkConfig +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.security.KeyStore +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + +/** + * Secure storage for Cardano wallet mnemonics using Android Keystore. + * + * Security model: + * - Mnemonic is encrypted with AES-256-GCM using a key stored in Android Keystore + * - The keystore key is device-bound (cannot be extracted) + * - The key is accessible when the device is unlocked (no additional biometric required for storage) + * - Transaction signing should use BiometricPrompt separately for user confirmation + */ +@ContributesBinding(AppScope::class) +class CardanoKeyStorageImpl @Inject constructor( + @ApplicationContext private val context: Context, +) : CardanoKeyStorage { + + companion object { + private const val ANDROID_KEYSTORE = "AndroidKeyStore" + private const val PREFS_NAME = "cardano_wallet_storage" + private const val KEY_ENCRYPTED_MNEMONIC_PREFIX = "encrypted_mnemonic_" + private const val KEY_IV_PREFIX = "iv_" + private const val KEYSTORE_ALIAS_PREFIX = "cardano_wallet_" + private const val CIPHER_TRANSFORMATION = "AES/GCM/NoPadding" + private const val GCM_TAG_LENGTH = 128 + private const val AES_KEY_SIZE = 256 + private const val MNEMONIC_ENTROPY_BYTES = 32 + } + + private val keyStore: KeyStore by lazy { + KeyStore.getInstance(ANDROID_KEYSTORE).apply { + load(null) + } + } + + private val prefs: SharedPreferences by lazy { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + } + + override suspend fun hasWallet(sessionId: SessionId): Boolean = withContext(Dispatchers.IO) { + val key = KEY_ENCRYPTED_MNEMONIC_PREFIX + sanitizeSessionId(sessionId) + prefs.contains(key) + } + + override suspend fun generateWallet(sessionId: SessionId): Result = + withContext(Dispatchers.IO) { + runCatching { + if (hasWallet(sessionId)) { + throw IllegalStateException("Wallet already exists for session: ${sessionId.value}") + } + + val entropy = ByteArray(MNEMONIC_ENTROPY_BYTES) + SecureRandom().nextBytes(entropy) + + val mnemonicCode = MnemonicCode() + val wordList = mnemonicCode.toMnemonic(entropy) + + entropy.fill(0) + + storeMnemonic(sessionId, wordList) + + val mnemonicString = wordList.joinToString(" ") + val account = Account(CardanoNetworkConfig.getNetwork(), mnemonicString) + + val result = WalletCreationResult( + mnemonic = wordList, + baseAddress = account.baseAddress(), + stakeAddress = account.stakeAddress(), + ) + + Timber.i("Generated new Cardano wallet for session: ${sessionId.value}") + result + } + } + + override suspend fun importWallet(sessionId: SessionId, mnemonic: List): Result = + withContext(Dispatchers.IO) { + runCatching { + if (hasWallet(sessionId)) { + throw IllegalStateException("Wallet already exists for session: ${sessionId.value}") + } + + require(mnemonic.size in listOf(12, 15, 18, 21, 24)) { + "Invalid mnemonic length: ${mnemonic.size} words. Expected 12, 15, 18, 21, or 24." + } + + val mnemonicCode = MnemonicCode() + try { + mnemonicCode.check(mnemonic) + } catch (e: Exception) { + throw IllegalArgumentException("Invalid mnemonic: ${e.message}") + } + + val mnemonicString = mnemonic.joinToString(" ") + val account = try { + Account(CardanoNetworkConfig.getNetwork(), mnemonicString) + } catch (e: Exception) { + throw IllegalArgumentException("Failed to derive Cardano keys: ${e.message}") + } + + storeMnemonic(sessionId, mnemonic) + + Timber.i("Imported Cardano wallet for session: ${sessionId.value}") + account.baseAddress() + } + } + + override suspend fun getMnemonic(sessionId: SessionId): Result> = + withContext(Dispatchers.IO) { + runCatching { + retrieveMnemonic(sessionId) + } + } + + override suspend fun getBaseAddress(sessionId: SessionId, addressIndex: Int): Result = + withContext(Dispatchers.IO) { + runCatching { + val mnemonic = retrieveMnemonic(sessionId) + val mnemonicString = mnemonic.joinToString(" ") + val account = Account(CardanoNetworkConfig.getNetwork(), mnemonicString, addressIndex) + account.baseAddress() + } + } + + override suspend fun getStakeAddress(sessionId: SessionId): Result = + withContext(Dispatchers.IO) { + runCatching { + val mnemonic = retrieveMnemonic(sessionId) + val mnemonicString = mnemonic.joinToString(" ") + val account = Account(CardanoNetworkConfig.getNetwork(), mnemonicString) + account.stakeAddress() + } + } + + override suspend fun deleteWallet(sessionId: SessionId): Result = + withContext(Dispatchers.IO) { + runCatching { + val sanitizedId = sanitizeSessionId(sessionId) + + prefs.edit() + .remove(KEY_ENCRYPTED_MNEMONIC_PREFIX + sanitizedId) + .remove(KEY_IV_PREFIX + sanitizedId) + .apply() + + val alias = KEYSTORE_ALIAS_PREFIX + sanitizedId + if (keyStore.containsAlias(alias)) { + keyStore.deleteEntry(alias) + } + + Timber.i("Deleted Cardano wallet for session: ${sessionId.value}") + } + } + + /** + * Get or create the AES secret key for encrypting the mnemonic. + * + * The key is: + * - Stored in Android Keystore (hardware-backed when available) + * - Device-bound (cannot be extracted) + * - Accessible when device is unlocked (no additional auth required) + * + * Note: Biometric/PIN confirmation for transactions should be handled separately + * at the transaction signing layer, not at the storage layer. + */ + private fun getOrCreateSecretKey(sessionId: SessionId): SecretKey { + val alias = KEYSTORE_ALIAS_PREFIX + sanitizeSessionId(sessionId) + + val existingKey = keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry + if (existingKey != null) { + return existingKey.secretKey + } + + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE) + + // Key spec: device-protected, no additional user authentication required + // This allows wallet operations when device is unlocked + // Transaction signing should use BiometricPrompt separately for confirmation + val keySpec = KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(AES_KEY_SIZE) + .build() + + keyGenerator.init(keySpec) + Timber.d("Created new keystore key for wallet: $alias") + return keyGenerator.generateKey() + } + + private fun storeMnemonic(sessionId: SessionId, mnemonic: List) { + val sanitizedId = sanitizeSessionId(sessionId) + val secretKey = getOrCreateSecretKey(sessionId) + + val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + + val mnemonicBytes = mnemonic.joinToString(" ").toByteArray(Charsets.UTF_8) + val encryptedBytes = cipher.doFinal(mnemonicBytes) + + mnemonicBytes.fill(0) + + prefs.edit() + .putString(KEY_ENCRYPTED_MNEMONIC_PREFIX + sanitizedId, Base64.encodeToString(encryptedBytes, Base64.NO_WRAP)) + .putString(KEY_IV_PREFIX + sanitizedId, Base64.encodeToString(cipher.iv, Base64.NO_WRAP)) + .apply() + } + + private fun retrieveMnemonic(sessionId: SessionId): List { + val sanitizedId = sanitizeSessionId(sessionId) + + val encryptedB64 = prefs.getString(KEY_ENCRYPTED_MNEMONIC_PREFIX + sanitizedId, null) + ?: throw IllegalStateException("No wallet found for session: ${sessionId.value}") + + val ivB64 = prefs.getString(KEY_IV_PREFIX + sanitizedId, null) + ?: throw IllegalStateException("Missing IV for session: ${sessionId.value}") + + val encryptedBytes = Base64.decode(encryptedB64, Base64.NO_WRAP) + val iv = Base64.decode(ivB64, Base64.NO_WRAP) + + val secretKey = try { + getOrCreateSecretKey(sessionId) + } catch (e: KeyPermanentlyInvalidatedException) { + Timber.e(e, "Key invalidated for session: ${sessionId.value}") + throw e + } + + val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION) + val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + + val decryptedBytes = cipher.doFinal(encryptedBytes) + val mnemonicString = String(decryptedBytes, Charsets.UTF_8) + + decryptedBytes.fill(0) + + return mnemonicString.split(" ") + } + + private fun sanitizeSessionId(sessionId: SessionId): String { + return sessionId.value + .replace("@", "") + .replace(":", "_") + .replace(".", "_") + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemContentPaymentFactory.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemContentPaymentFactory.kt new file mode 100644 index 0000000000..21f7034ac5 --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemContentPaymentFactory.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.timeline + +import dev.zacsweers.metro.Inject +import io.element.android.features.wallet.api.PaymentCardStatus +import io.element.android.features.wallet.api.timeline.TimelineItemPaymentContent +import io.element.android.features.wallet.impl.payment.DefaultPaymentEventSender +import io.element.android.features.wallet.impl.payment.PaymentEventData +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.json.JsonNull +import timber.log.Timber + +/** + * Factory for creating [TimelineItemPaymentContent] from message content. + * + * Parses messages with the $CARDANO_PAY$ prefix and extracts payment data. + */ +@Inject +class TimelineItemContentPaymentFactory { + private val json = Json { + ignoreUnknownKeys = true + isLenient = true + } + + /** + * Check if a message is a payment message. + */ + /** + * Check if an event type is a payment event type. + */ + fun isPaymentEventType(eventType: String): Boolean { + return eventType == TimelineItemPaymentContent.EVENT_TYPE || + eventType == "co.sulkta.payment.status" + } + + fun isPaymentMessage(body: String): Boolean { + return body.startsWith(DefaultPaymentEventSender.PAYMENT_MESSAGE_PREFIX) + } + + /** + * Check if a message is a status update message. + */ + fun isStatusUpdateMessage(body: String): Boolean { + return body.startsWith(DefaultPaymentEventSender.STATUS_MESSAGE_PREFIX) + } + + /** + * Create a [TimelineItemPaymentContent] from a message body. + * + * @param body The message body + * @param isSentByMe Whether the current user sent this message + * @return The parsed payment content, or null if parsing failed + */ + fun createFromMessage(body: String, isSentByMe: Boolean): TimelineItemPaymentContent? { + return try { + val jsonContent = when { + body.startsWith(DefaultPaymentEventSender.PAYMENT_MESSAGE_PREFIX) -> { + body.removePrefix(DefaultPaymentEventSender.PAYMENT_MESSAGE_PREFIX) + } + body.startsWith(DefaultPaymentEventSender.STATUS_MESSAGE_PREFIX) -> { + // Status updates don't create full payment content + return null + } + else -> return null + } + + val data = json.decodeFromString(jsonContent) + TimelineItemPaymentContent( + amountLovelace = data.amountLovelace, + toAddress = data.toAddress, + fromAddress = data.fromAddress, + txHash = data.txHash, + status = parseStatus(data.status), + network = data.network, + isSentByMe = isSentByMe, + fallbackText = "💰 ${TimelineItemPaymentContent.formatAda(data.amountLovelace)}", + ) + } catch (e: Exception) { + Timber.w(e, "Failed to parse payment message") + null + } + } + + /** + * Create a [TimelineItemPaymentContent] from raw JSON event content (legacy support). + */ + fun createFromRaw(rawJson: String, isSentByMe: Boolean): TimelineItemPaymentContent? { + return try { + val eventJson = json.parseToJsonElement(rawJson).jsonObject + val content = eventJson["content"]?.jsonObject ?: eventJson + + val data = parsePaymentData(content) + if (data != null) { + TimelineItemPaymentContent( + amountLovelace = data.amountLovelace, + toAddress = data.toAddress, + fromAddress = data.fromAddress, + txHash = data.txHash, + status = parseStatus(data.status), + network = data.network, + isSentByMe = isSentByMe, + fallbackText = "💰 ${TimelineItemPaymentContent.formatAda(data.amountLovelace)}", + ) + } else { + null + } + } catch (e: Exception) { + Timber.w(e, "Failed to parse payment event from raw JSON") + null + } + } + + /** + * Create a [TimelineItemPaymentContent] from parsed payment data. + */ + fun createFromData( + data: PaymentEventData, + isSentByMe: Boolean, + ): TimelineItemPaymentContent { + return TimelineItemPaymentContent( + amountLovelace = data.amountLovelace, + toAddress = data.toAddress, + fromAddress = data.fromAddress, + txHash = data.txHash, + status = parseStatus(data.status), + network = data.network, + isSentByMe = isSentByMe, + fallbackText = "💰 ${TimelineItemPaymentContent.formatAda(data.amountLovelace)}", + ) + } + + private fun parsePaymentData(content: JsonObject): PaymentEventData? { + return try { + val amountLovelace = content["amount_lovelace"]?.jsonPrimitive?.longOrNull + ?: content["amountLovelace"]?.jsonPrimitive?.longOrNull + ?: return null + + val toAddress = content["to_address"]?.jsonPrimitive?.content + ?: content["toAddress"]?.jsonPrimitive?.content + ?: return null + + val fromAddress = content["from_address"]?.jsonPrimitive?.content + ?: content["fromAddress"]?.jsonPrimitive?.content + ?: return null + + val txHash = content["tx_hash"]?.takeUnless { it is JsonNull }?.jsonPrimitive?.content + ?: content["txHash"]?.takeUnless { it is JsonNull }?.jsonPrimitive?.content + + val status = content["status"]?.jsonPrimitive?.content ?: "pending" + val network = content["network"]?.jsonPrimitive?.content ?: "mainnet" + + PaymentEventData( + amountLovelace = amountLovelace, + toAddress = toAddress, + fromAddress = fromAddress, + txHash = txHash, + status = status, + network = network, + ) + } catch (e: Exception) { + Timber.w(e, "Failed to parse payment data from JSON object") + null + } + } + + private fun parseStatus(status: String): PaymentCardStatus { + return when (status.lowercase()) { + "pending" -> PaymentCardStatus.PENDING + "confirmed" -> PaymentCardStatus.CONFIRMED + "failed" -> PaymentCardStatus.FAILED + else -> PaymentCardStatus.PENDING + } + } +} diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemPaymentView.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemPaymentView.kt new file mode 100644 index 0000000000..4d4b8f0f4f --- /dev/null +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemPaymentView.kt @@ -0,0 +1,337 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.element.android.features.wallet.impl.timeline + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.wallet.api.PaymentCardStatus +import io.element.android.features.wallet.api.timeline.TimelineItemPaymentContent +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +/** + * Composable for rendering a Cardano payment card in the timeline. + * + * The card displays: + * - ADA icon and amount + * - Recipient/sender address (truncated) + * - Status indicator (spinner for pending, checkmark for confirmed, X for failed) + * - Truncated transaction hash (tappable to open CardanoScan) + * - Testnet badge when applicable + * + * Alignment varies based on sender: + * - Sent by me: right-aligned with primary color + * - Received: left-aligned with surface color + */ +@Composable +fun TimelineItemPaymentView( + content: TimelineItemPaymentContent, + modifier: Modifier = Modifier, +) { + val uriHandler = LocalUriHandler.current + val backgroundColor = if (content.isSentByMe) { + ElementTheme.colors.bgActionPrimaryRest + } else { + MaterialTheme.colorScheme.surfaceVariant + } + val contentColor = if (content.isSentByMe) { + Color.White + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + + Surface( + modifier = modifier + .fillMaxWidth(0.85f) + .clip(RoundedCornerShape(16.dp)), + color = backgroundColor, + tonalElevation = 2.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + ) { + // Header row with icon and testnet badge + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + // Cardano icon (hexagon shape) + CardanoIcon( + color = contentColor, + modifier = Modifier.size(24.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = if (content.isSentByMe) "Sent" else "Received", + style = MaterialTheme.typography.labelMedium, + color = contentColor.copy(alpha = 0.7f), + ) + } + + if (content.isTestnet) { + TestnetBadge() + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Amount - large and prominent + Text( + text = content.amountAda, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = contentColor, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Recipient/sender address + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = if (content.isSentByMe) "To: " else "From: ", + style = MaterialTheme.typography.bodySmall, + color = contentColor.copy(alpha = 0.6f), + ) + Text( + text = if (content.isSentByMe) content.truncatedToAddress else content.truncatedFromAddress, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, + color = contentColor.copy(alpha = 0.8f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Status row with tx hash + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + PaymentStatusChip( + status = content.status, + contentColor = contentColor, + ) + + // Transaction hash (if available) + content.truncatedTxHash?.let { hash -> + Text( + text = hash, + style = MaterialTheme.typography.bodySmall, + color = contentColor.copy(alpha = 0.6f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.clickable { + content.explorerUrl?.let { url -> + uriHandler.openUri(url) + } + }, + ) + } + } + + // View on explorer link (only for confirmed with tx hash) + if (content.status == PaymentCardStatus.CONFIRMED && content.explorerUrl != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "View on CardanoScan →", + style = MaterialTheme.typography.labelSmall, + color = contentColor.copy(alpha = 0.8f), + modifier = Modifier.clickable { + uriHandler.openUri(content.explorerUrl!!) + }, + ) + } + } + } +} + +@Composable +private fun CardanoIcon( + color: Color, + modifier: Modifier = Modifier, +) { + // Using a hexagon-like shape for Cardano + // In production, replace with actual Cardano logo asset + Box( + modifier = modifier + .clip(CircleShape) + .background(color.copy(alpha = 0.2f)), + contentAlignment = Alignment.Center, + ) { + Text( + text = "₳", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = color, + ) + } +} + +@Composable +private fun TestnetBadge() { + Surface( + shape = RoundedCornerShape(4.dp), + color = MaterialTheme.colorScheme.errorContainer, + ) { + Text( + text = "TESTNET", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + ) + } +} + +@Composable +private fun PaymentStatusChip( + status: PaymentCardStatus, + contentColor: Color, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background( + color = contentColor.copy(alpha = 0.1f), + shape = RoundedCornerShape(12.dp), + ) + .padding(horizontal = 10.dp, vertical = 4.dp), + ) { + when (status) { + PaymentCardStatus.PENDING -> { + CircularProgressIndicator( + modifier = Modifier.size(14.dp), + strokeWidth = 2.dp, + color = contentColor, + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "Pending", + style = MaterialTheme.typography.labelMedium, + color = contentColor, + ) + } + PaymentCardStatus.CONFIRMED -> { + Icon( + imageVector = CompoundIcons.Check(), + contentDescription = "Confirmed", + modifier = Modifier.size(14.dp), + tint = Color(0xFF4CAF50), // Green + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "Confirmed", + style = MaterialTheme.typography.labelMedium, + color = contentColor, + ) + } + PaymentCardStatus.FAILED -> { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = "Failed", + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.error, + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "Failed", + style = MaterialTheme.typography.labelMedium, + color = contentColor, + ) + } + } + } +} + +// Preview parameter provider +private class PaymentContentPreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + // Sent, pending, testnet + TimelineItemPaymentContent( + amountLovelace = 10_000_000, + toAddress = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp", + fromAddress = "addr_test1qp2fg770ddmqxxduasjsas39l5wwvwa04nj8ud95fde7f70k6tew7wrnx0s4465nx05ajyn65aa3ljgqprv62tuys9rqhpd0hq", + txHash = "abc123def456789012345678901234567890", + status = PaymentCardStatus.PENDING, + network = "testnet", + isSentByMe = true, + fallbackText = "💰 Sent 10 ADA", + ), + // Received, confirmed, mainnet + TimelineItemPaymentContent( + amountLovelace = 5_500_000, + toAddress = "addr1qp2fg770ddmqxxduasjsas39l5wwvwa04nj8ud95fde7f70k6tew7wrnx0s4465nx05ajyn65aa3ljgqprv62tuys9rqhpd0hq", + fromAddress = "addr1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp", + txHash = "xyz789abc123def456789012345678901234", + status = PaymentCardStatus.CONFIRMED, + network = "mainnet", + isSentByMe = false, + fallbackText = "💰 Received 5.5 ADA", + ), + // Sent, failed + TimelineItemPaymentContent( + amountLovelace = 100_000_000, + toAddress = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp", + fromAddress = "addr_test1qp2fg770ddmqxxduasjsas39l5wwvwa04nj8ud95fde7f70k6tew7wrnx0s4465nx05ajyn65aa3ljgqprv62tuys9rqhpd0hq", + txHash = null, + status = PaymentCardStatus.FAILED, + network = "testnet", + isSentByMe = true, + fallbackText = "💰 Sent 100 ADA", + ), + ) +} + +@PreviewsDayNight +@Composable +internal fun TimelineItemPaymentViewPreview( + @PreviewParameter(PaymentContentPreviewProvider::class) content: TimelineItemPaymentContent, +) = ElementPreview { + TimelineItemPaymentView( + content = content, + modifier = Modifier.padding(16.dp), + ) +} diff --git a/features/wallet/impl/src/main/res/values/strings.xml b/features/wallet/impl/src/main/res/values/strings.xml new file mode 100644 index 0000000000..c39a93a16d --- /dev/null +++ b/features/wallet/impl/src/main/res/values/strings.xml @@ -0,0 +1,76 @@ + + + + Cardano Wallet + + + Overview + Assets + History + Settings + + + Balance + Testnet + QR code for receiving ADA + Copy address + Tap to copy full address + Send ADA + + + No native assets yet + + + No transactions yet + Sent + Received + View on explorer + + + Wallet Address + No wallet configured + Copy full address + Network + Preprod Testnet + Mainnet + Export Recovery Phrase + View your 24-word recovery phrase + Delete Wallet + Remove wallet from this device + + + Set up your wallet + Your Cardano wallet keys will be stored securely on your device and backed up via your Matrix account. + Get Started + Restore from Matrix Backup + + + Set up your wallet to send ADA + Set Up Wallet + Insufficient balance (%s ADA available) + + + Backup to Matrix + Encrypt and store your wallet in Matrix account data + Restore from Matrix + Restore wallet from Matrix backup + Enter Recovery Key + Enter your Matrix recovery key to encrypt your wallet backup. This is the same key used to unlock your encrypted messages. + Recovery key + Backup + Restore + Cancel + Wallet backed up successfully + Wallet restored successfully + Backup failed: %s + Restore failed: %s + + + Delete Wallet? + This will permanently remove your wallet from this device. If you haven\'t backed up your recovery phrase, you will lose access to your funds forever. + Make sure you have: + • Written down your 24-word recovery phrase, OR\n• Backed up to Matrix + Delete Wallet + Cancel + Wallet deleted + diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/WalletStateTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/WalletStateTest.kt new file mode 100644 index 0000000000..b533ef333b --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/WalletStateTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wallet.api.WalletState +import org.junit.Test + +class WalletStateTest { + @Test + fun `initial state has correct defaults`() { + val state = WalletState.Initial + + assertThat(state.hasWallet).isFalse() + assertThat(state.address).isNull() + assertThat(state.balanceLovelace).isNull() + assertThat(state.balanceAda).isNull() + assertThat(state.isLoading).isTrue() + assertThat(state.error).isNull() + } + + @Test + fun `state can be updated`() { + val state = WalletState( + hasWallet = true, + address = "addr1test", + balanceLovelace = 10_000_000L, + balanceAda = "10", + isLoading = false, + error = null, + ) + + assertThat(state.hasWallet).isTrue() + assertThat(state.address).isEqualTo("addr1test") + assertThat(state.balanceLovelace).isEqualTo(10_000_000L) + assertThat(state.balanceAda).isEqualTo("10") + assertThat(state.isLoading).isFalse() + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/CardanoNetworkConfigTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/CardanoNetworkConfigTest.kt new file mode 100644 index 0000000000..6d87c19563 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/CardanoNetworkConfigTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.cardano + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class CardanoNetworkConfigTest { + + @Test + fun `network is configured as testnet`() { + // Verify we are on testnet by default (as per Phase 1 requirements) + assertThat(CardanoNetworkConfig.NETWORK).isEqualTo(CardanoNetwork.TESTNET) + } + + @Test + fun `testnet has network ID 0`() { + // Testnet network ID should be 0 + assertThat(CardanoNetworkConfig.NETWORK_ID).isEqualTo(0) + } + + @Test + fun `testnet uses preprod Koios URL`() { + assertThat(CardanoNetworkConfig.KOIOS_BASE_URL).isEqualTo("https://preprod.koios.rest/api/v1/") + } + + @Test + fun `testnet uses preprod CardanoScan`() { + assertThat(CardanoNetworkConfig.EXPLORER_BASE_URL).isEqualTo("https://preprod.cardanoscan.io") + } + + @Test + fun `testnet address prefix is addr_test1`() { + assertThat(CardanoNetworkConfig.ADDRESS_PREFIX).isEqualTo("addr_test1") + } + + @Test + fun `network name is Preprod Testnet`() { + assertThat(CardanoNetworkConfig.NETWORK_NAME).isEqualTo("Preprod Testnet") + } + + @Test + fun `getNetwork returns preprod network`() { + val network = CardanoNetworkConfig.getNetwork() + + // Preprod network has protocol magic 1 + assertThat(network.protocolMagic).isEqualTo(1) + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManagerTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManagerTest.kt new file mode 100644 index 0000000000..8d667a83b4 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/CardanoWalletManagerTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.cardano + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wallet.test.FakeCardanoClient +import io.element.android.features.wallet.test.storage.FakeCardanoKeyStorage +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class CardanoWalletManagerTest { + + private lateinit var fakeKeyStorage: FakeCardanoKeyStorage + private lateinit var fakeCardanoClient: FakeCardanoClient + private lateinit var walletManager: DefaultCardanoWalletManager + private val testSessionId = UserId("@test:matrix.org") + private val testBaseAddress = "addr_test1qpfake" + private val testStakeAddress = "stake_test1upfake" + + @Before + fun setUp() { + fakeKeyStorage = FakeCardanoKeyStorage() + fakeCardanoClient = FakeCardanoClient() + walletManager = DefaultCardanoWalletManager(fakeKeyStorage) + } + + @Test + fun `initial state has no wallet`() = runTest { + val state = walletManager.walletState.value + + assertThat(state.hasWallet).isFalse() + assertThat(state.address).isNull() + assertThat(state.isLoading).isTrue() + } + + @Test + fun `initialize sets hasWallet false when no wallet exists`() = runTest { + walletManager.initialize(testSessionId) + + val state = walletManager.walletState.value + assertThat(state.hasWallet).isFalse() + assertThat(state.isLoading).isFalse() + assertThat(state.error).isNull() + } + + @Test + fun `initialize loads wallet when it exists`() = runTest { + // Create a wallet first + fakeKeyStorage.generateWallet(testSessionId) + + walletManager.initialize(testSessionId) + + val state = walletManager.walletState.value + assertThat(state.hasWallet).isTrue() + assertThat(state.address).isEqualTo(testBaseAddress) + assertThat(state.isLoading).isFalse() + } + + @Test + fun `initialize handles address fetch failure gracefully`() = runTest { + fakeKeyStorage.getBaseAddressResult = Result.failure(RuntimeException("Storage error")) + fakeKeyStorage.generateWallet(testSessionId) + + walletManager.initialize(testSessionId) + + val state = walletManager.walletState.value + // Wallet exists but address couldn't be loaded + assertThat(state.hasWallet).isTrue() + assertThat(state.address).isNull() + assertThat(state.isLoading).isFalse() + } + + @Test + fun `getAddress returns address from storage`() = runTest { + fakeKeyStorage.generateWallet(testSessionId) + + val result = walletManager.getAddress(testSessionId) + + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEqualTo(testBaseAddress) + } + + @Test + fun `getStakeAddress returns stake address from storage`() = runTest { + fakeKeyStorage.generateWallet(testSessionId) + + val result = walletManager.getStakeAddress(testSessionId) + + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEqualTo(testStakeAddress) + } + + @Test + fun `getAddress returns error when no wallet exists`() = runTest { + val result = walletManager.getAddress(testSessionId) + + assertThat(result.isFailure).isTrue() + } + + @Test + fun `clearState resets to initial`() = runTest { + fakeKeyStorage.generateWallet(testSessionId) + walletManager.initialize(testSessionId) + + walletManager.clearState() + + val state = walletManager.walletState.value + assertThat(state.hasWallet).isFalse() + assertThat(state.isLoading).isTrue() + } + + @Test + fun `different sessions have isolated wallets`() = runTest { + val session1 = UserId("@user1:matrix.org") + val session2 = UserId("@user2:matrix.org") + + fakeKeyStorage.generateWallet(session1) + + assertThat(fakeKeyStorage.hasWallet(session1)).isTrue() + assertThat(fakeKeyStorage.hasWallet(session2)).isFalse() + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/KoiosCardanoClientTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/KoiosCardanoClientTest.kt new file mode 100644 index 0000000000..beb7fe350a --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/KoiosCardanoClientTest.kt @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.cardano + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wallet.api.CardanoException +import io.element.android.features.wallet.api.TxStatus +import io.element.android.features.wallet.test.FakeCardanoClient +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +/** + * Unit tests for CardanoClient implementations. + * + * These tests use FakeCardanoClient to verify the contract + * that KoiosCardanoClient implements. Integration tests with + * real Koios API should be separate. + */ +class KoiosCardanoClientTest { + private lateinit var fakeClient: FakeCardanoClient + + @Before + fun setUp() { + fakeClient = FakeCardanoClient() + } + + @Test + fun `getBalance returns correct balance for known address`() = runTest { + // Given + val address = FakeCardanoClient.TEST_ADDRESS + val expectedBalance = 10_000_000L // 10 ADA + fakeClient.setupWallet(address, expectedBalance) + + // When + val result = fakeClient.getBalance(address) + + // Then + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEqualTo(expectedBalance) + assertThat(fakeClient.getBalanceCallCount).isEqualTo(1) + } + + @Test + fun `getBalance returns 0 for unknown address`() = runTest { + // Given + val unknownAddress = "addr_test1_unknown" + + // When + val result = fakeClient.getBalance(unknownAddress) + + // Then + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEqualTo(0L) + } + + @Test + fun `getBalance fails with network error when configured`() = runTest { + // Given + fakeClient.shouldFailWithNetworkError = true + + // When + val result = fakeClient.getBalance(FakeCardanoClient.TEST_ADDRESS) + + // Then + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(CardanoException.NetworkException::class.java) + } + + @Test + fun `getBalance fails with rate limit when configured`() = runTest { + // Given + fakeClient.shouldFailWithRateLimit = true + + // When + val result = fakeClient.getBalance(FakeCardanoClient.TEST_ADDRESS) + + // Then + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(CardanoException.RateLimitException::class.java) + } + + @Test + fun `getUtxos returns correct UTxOs for address with balance`() = runTest { + // Given + val address = FakeCardanoClient.TEST_ADDRESS + val balance = 5_000_000L // 5 ADA + fakeClient.setupWallet(address, balance) + + // When + val result = fakeClient.getUtxos(address) + + // Then + assertThat(result.isSuccess).isTrue() + val utxos = result.getOrNull()!! + assertThat(utxos).isNotEmpty() + assertThat(utxos.sumOf { it.amount }).isEqualTo(balance) + assertThat(utxos.all { it.address == address }).isTrue() + } + + @Test + fun `getUtxos returns empty list for address with no balance`() = runTest { + // Given + val address = "addr_test1_empty" + + // When + val result = fakeClient.getUtxos(address) + + // Then + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEmpty() + } + + @Test + fun `submitTx returns tx hash on success`() = runTest { + // Given + val txCbor = "84a400818258203b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b700018182583900de0f5a6d9a3e0e7f8b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a821a001e8480a1581c0000000000000000000000000000000000000000000000000000000a14574657374011a00989680021a0002917d031a04bea742" + + // When + val result = fakeClient.submitTx(txCbor) + + // Then + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).startsWith("fake_tx_") + assertThat(fakeClient.submitTxCallCount).isEqualTo(1) + assertThat(fakeClient.submittedTransactions).hasSize(1) + } + + @Test + fun `submitTx fails when configured to fail`() = runTest { + // Given + fakeClient.submitShouldFail = true + fakeClient.submitErrorMessage = "Insufficient funds" + + // When + val result = fakeClient.submitTx("dummy_cbor") + + // Then + assertThat(result.isFailure).isTrue() + val exception = result.exceptionOrNull() as CardanoException.SubmissionFailedException + assertThat(exception.message).contains("Insufficient funds") + } + + @Test + fun `getTxStatus returns PENDING for newly submitted tx`() = runTest { + // Given + val submitResult = fakeClient.submitTx("dummy_cbor") + val txHash = submitResult.getOrThrow() + + // When + val result = fakeClient.getTxStatus(txHash) + + // Then + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEqualTo(TxStatus.PENDING) + } + + @Test + fun `getTxStatus returns CONFIRMED after confirmation`() = runTest { + // Given + val submitResult = fakeClient.submitTx("dummy_cbor") + val txHash = submitResult.getOrThrow() + fakeClient.confirmTransaction(txHash) + + // When + val result = fakeClient.getTxStatus(txHash) + + // Then + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEqualTo(TxStatus.CONFIRMED) + } + + @Test + fun `getTxStatus returns FAILED for failed tx`() = runTest { + // Given + val txHash = "some_tx_hash" + fakeClient.transactionStatuses[txHash] = TxStatus.PENDING + fakeClient.failTransaction(txHash) + + // When + val result = fakeClient.getTxStatus(txHash) + + // Then + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEqualTo(TxStatus.FAILED) + } + + @Test + fun `reset clears all state`() = runTest { + // Given + fakeClient.setupWallet(FakeCardanoClient.TEST_ADDRESS, 1_000_000L) + fakeClient.submitTx("dummy") + fakeClient.shouldFailWithNetworkError = true + + // When + fakeClient.reset() + + // Then + assertThat(fakeClient.balances).isEmpty() + assertThat(fakeClient.utxos).isEmpty() + assertThat(fakeClient.submittedTransactions).isEmpty() + assertThat(fakeClient.shouldFailWithNetworkError).isFalse() + assertThat(fakeClient.submitTxCallCount).isEqualTo(0) + } + + @Test + fun `createDefaultUtxos creates valid UTxOs summing to total`() { + // Given + val address = FakeCardanoClient.TEST_ADDRESS + val total = 15_000_000L // 15 ADA + + // When + val utxos = FakeCardanoClient.createDefaultUtxos(address, total) + + // Then + assertThat(utxos).isNotEmpty() + assertThat(utxos.sumOf { it.amount }).isEqualTo(total) + utxos.forEach { utxo -> + assertThat(utxo.address).isEqualTo(address) + assertThat(utxo.txHash).hasLength(64) // 32 bytes hex + assertThat(utxo.outputIndex).isAtLeast(0) + } + } + + @Test + fun `createDefaultUtxos returns empty list for zero balance`() { + // When + val utxos = FakeCardanoClient.createDefaultUtxos(FakeCardanoClient.TEST_ADDRESS, 0L) + + // Then + assertThat(utxos).isEmpty() + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/PaymentStatusPollerTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/PaymentStatusPollerTest.kt new file mode 100644 index 0000000000..6b06f49074 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/PaymentStatusPollerTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.cardano + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wallet.api.PaymentStatusPoller +import io.element.android.features.wallet.api.TxStatus +import io.element.android.features.wallet.test.FakeCardanoClient +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +/** + * Unit tests for [PaymentStatusPoller] implementation. + */ +class PaymentStatusPollerTest { + private lateinit var fakeClient: FakeCardanoClient + private lateinit var poller: PaymentStatusPoller + + @Before + fun setUp() { + fakeClient = FakeCardanoClient() + poller = DefaultPaymentStatusPoller(fakeClient) + } + + @Test + fun `pollUntilConfirmed emits PENDING initially`() = runTest { + val txHash = "test_tx_hash_abc123" + + poller.pollUntilConfirmed(txHash).test { + val firstStatus = awaitItem() + assertThat(firstStatus).isEqualTo(TxStatus.PENDING) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `pollUntilConfirmed emits CONFIRMED when transaction confirms`() = runTest { + val txHash = "test_tx_hash_abc123" + fakeClient.transactionStatuses[txHash] = TxStatus.CONFIRMED + + poller.pollUntilConfirmed(txHash).test { + assertThat(awaitItem()).isEqualTo(TxStatus.PENDING) + assertThat(awaitItem()).isEqualTo(TxStatus.CONFIRMED) + awaitComplete() + } + } + + @Test + fun `pollUntilConfirmed emits FAILED when transaction fails`() = runTest { + val txHash = "test_tx_hash_abc123" + fakeClient.transactionStatuses[txHash] = TxStatus.FAILED + + poller.pollUntilConfirmed(txHash).test { + assertThat(awaitItem()).isEqualTo(TxStatus.PENDING) + assertThat(awaitItem()).isEqualTo(TxStatus.FAILED) + awaitComplete() + } + } + + @Test + fun `pollUntilConfirmed calls getTxStatus at least once`() = runTest { + val txHash = "test_tx_pending_tx" + fakeClient.transactionStatuses[txHash] = TxStatus.CONFIRMED + + poller.pollUntilConfirmed(txHash).test { + assertThat(awaitItem()).isEqualTo(TxStatus.PENDING) + assertThat(awaitItem()).isEqualTo(TxStatus.CONFIRMED) + awaitComplete() + } + + // Verify getTxStatus was called + assertThat(fakeClient.getTxStatusCallCount).isAtLeast(1) + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/TransactionBuilderTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/TransactionBuilderTest.kt new file mode 100644 index 0000000000..0009ded3ef --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/cardano/TransactionBuilderTest.kt @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.cardano + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wallet.api.CardanoException +import io.element.android.features.wallet.api.PaymentRequest +import io.element.android.features.wallet.api.SignedTransaction +import io.element.android.features.wallet.api.Utxo +import io.element.android.features.wallet.test.FakeCardanoClient +import io.element.android.features.wallet.test.FakeTransactionBuilder +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +/** + * Unit tests for [TransactionBuilder] implementations. + * + * Tests cover: + * - UTXO selection logic (sufficient, insufficient, exact amount, multiple UTXOs) + * - Fee calculation validation + * - Error handling (insufficient funds, invalid address, etc.) + * - FakeTransactionBuilder behavior for presenter tests + */ +class TransactionBuilderTest { + private lateinit var fakeClient: FakeCardanoClient + private lateinit var fakeBuilder: FakeTransactionBuilder + + private val testSessionId = SessionId("@test:matrix.org") + private val senderAddress = FakeCardanoClient.TEST_ADDRESS + private val recipientAddress = "addr_test1qp2fg..." + "a".repeat(80) // Valid-length address + + @Before + fun setUp() { + fakeClient = FakeCardanoClient() + fakeBuilder = FakeTransactionBuilder() + } + + // ==================== FakeTransactionBuilder Tests ==================== + + @Test + fun `FakeTransactionBuilder returns success by default`() = runTest { + // Given + val request = createPaymentRequest(10_000_000L) // 10 ADA + + // When + val result = fakeBuilder.buildAndSign(request) + + // Then + assertThat(result.isSuccess).isTrue() + val tx = result.getOrNull()!! + assertThat(tx.txHash).startsWith("fake_tx_") + assertThat(tx.fee).isEqualTo(180_000L) // Default fee + assertThat(tx.actualAmount).isEqualTo(10_000_000L) + assertThat(tx.txCbor).isNotEmpty() + } + + @Test + fun `FakeTransactionBuilder tracks calls correctly`() = runTest { + // Given + val request1 = createPaymentRequest(5_000_000L) + val request2 = createPaymentRequest(10_000_000L) + + // When + fakeBuilder.buildAndSign(request1) + fakeBuilder.buildAndSign(request2) + + // Then + assertThat(fakeBuilder.buildAndSignCallCount).isEqualTo(2) + assertThat(fakeBuilder.buildAndSignCalls).hasSize(2) + assertThat(fakeBuilder.buildAndSignCalls[0].amountLovelace).isEqualTo(5_000_000L) + assertThat(fakeBuilder.buildAndSignCalls[1].amountLovelace).isEqualTo(10_000_000L) + } + + @Test + fun `FakeTransactionBuilder returns insufficient funds error when configured`() = runTest { + // Given + fakeBuilder.givenInsufficientFunds(available = 5_000_000L, required = 10_000_000L) + val request = createPaymentRequest(10_000_000L) + + // When + val result = fakeBuilder.buildAndSign(request) + + // Then + assertThat(result.isFailure).isTrue() + val exception = result.exceptionOrNull() as CardanoException.InsufficientFundsException + assertThat(exception.available).isEqualTo(5_000_000L) + assertThat(exception.required).isEqualTo(10_000_000L) + } + + @Test + fun `FakeTransactionBuilder returns invalid address error when configured`() = runTest { + // Given + val badAddress = "invalid_address" + fakeBuilder.givenInvalidAddress(badAddress) + val request = createPaymentRequest(10_000_000L) + + // When + val result = fakeBuilder.buildAndSign(request) + + // Then + assertThat(result.isFailure).isTrue() + val exception = result.exceptionOrNull() as CardanoException.InvalidAddressException + assertThat(exception.address).isEqualTo(badAddress) + } + + @Test + fun `FakeTransactionBuilder returns network error when configured`() = runTest { + // Given + fakeBuilder.givenNetworkError("Connection timeout") + val request = createPaymentRequest(10_000_000L) + + // When + val result = fakeBuilder.buildAndSign(request) + + // Then + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(CardanoException.NetworkException::class.java) + } + + @Test + fun `FakeTransactionBuilder can return custom result`() = runTest { + // Given + val customTx = SignedTransaction( + txCbor = "custom_cbor", + txHash = "custom_hash_abc123", + fee = 250_000L, + actualAmount = 7_500_000L, + ) + fakeBuilder.givenResult(Result.success(customTx)) + val request = createPaymentRequest(7_500_000L) + + // When + val result = fakeBuilder.buildAndSign(request) + + // Then + assertThat(result.isSuccess).isTrue() + val tx = result.getOrNull()!! + assertThat(tx.txHash).isEqualTo("custom_hash_abc123") + assertThat(tx.fee).isEqualTo(250_000L) + } + + @Test + fun `FakeTransactionBuilder verifyBuildAndSignCalled works correctly`() = runTest { + // Given + val request = createPaymentRequest(10_000_000L) + fakeBuilder.buildAndSign(request) + + // Then + assertThat(fakeBuilder.verifyBuildAndSignCalled(fromAddress = senderAddress)).isTrue() + assertThat(fakeBuilder.verifyBuildAndSignCalled(amountLovelace = 10_000_000L)).isTrue() + assertThat(fakeBuilder.verifyBuildAndSignCalled(amountLovelace = 99_999_999L)).isFalse() + } + + @Test + fun `FakeTransactionBuilder reset clears all state`() = runTest { + // Given + fakeBuilder.buildAndSign(createPaymentRequest(10_000_000L)) + fakeBuilder.givenInsufficientFunds(1L, 2L) + + // When + fakeBuilder.reset() + + // Then + assertThat(fakeBuilder.buildAndSignCallCount).isEqualTo(0) + assertThat(fakeBuilder.buildAndSignCalls).isEmpty() + assertThat(fakeBuilder.shouldSucceed).isTrue() + assertThat(fakeBuilder.errorToThrow).isNull() + } + + @Test + fun `FakeTransactionBuilder companion creates success builder`() = runTest { + // Given + val builder = FakeTransactionBuilder.success(fee = 200_000L) + + // When + val result = builder.buildAndSign(createPaymentRequest(5_000_000L)) + + // Then + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()!!.fee).isEqualTo(200_000L) + } + + @Test + fun `FakeTransactionBuilder companion creates insufficient funds builder`() = runTest { + // Given + val builder = FakeTransactionBuilder.insufficientFunds( + available = 1_000_000L, + required = 10_000_000L + ) + + // When + val result = builder.buildAndSign(createPaymentRequest(10_000_000L)) + + // Then + assertThat(result.isFailure).isTrue() + val exception = result.exceptionOrNull() as CardanoException.InsufficientFundsException + assertThat(exception.available).isEqualTo(1_000_000L) + } + + // ==================== UTXO Selection Tests ==================== + // These test the FakeCardanoClient UTXO setup logic + + @Test + fun `UTXO selection - single UTXO covers amount`() = runTest { + // Given + val balance = 20_000_000L // 20 ADA + val utxos = listOf( + createUtxo("tx1", 0, 20_000_000L) + ) + fakeClient.balances[senderAddress] = balance + fakeClient.utxos[senderAddress] = utxos + + // When + val result = fakeClient.getUtxos(senderAddress) + + // Then + assertThat(result.isSuccess).isTrue() + val fetchedUtxos = result.getOrNull()!! + assertThat(fetchedUtxos).hasSize(1) + assertThat(fetchedUtxos.sumOf { it.amount }).isGreaterThan(10_000_000L + 200_000L) + } + + @Test + fun `UTXO selection - multiple UTXOs needed to cover amount`() = runTest { + // Given: 3 small UTXOs that together cover the amount + val utxos = listOf( + createUtxo("tx1", 0, 3_000_000L), + createUtxo("tx2", 0, 4_000_000L), + createUtxo("tx3", 0, 5_000_000L), + ) + fakeClient.balances[senderAddress] = 12_000_000L + fakeClient.utxos[senderAddress] = utxos + + // When + val result = fakeClient.getUtxos(senderAddress) + + // Then + assertThat(result.isSuccess).isTrue() + val fetchedUtxos = result.getOrNull()!! + assertThat(fetchedUtxos).hasSize(3) + assertThat(fetchedUtxos.sumOf { it.amount }).isEqualTo(12_000_000L) + } + + @Test + fun `UTXO selection - exact amount matches available`() = runTest { + // Given: Exact amount (plus estimated fee) equals total UTXOs + val balance = 10_200_000L // 10.2 ADA (covers 10 ADA + ~200k fee) + fakeClient.setupWallet(senderAddress, balance) + + // When + val utxosResult = fakeClient.getUtxos(senderAddress) + + // Then + assertThat(utxosResult.isSuccess).isTrue() + assertThat(utxosResult.getOrNull()!!.sumOf { it.amount }).isEqualTo(balance) + } + + @Test + fun `UTXO selection - insufficient funds returns empty or low balance`() = runTest { + // Given: Not enough balance + val balance = 500_000L // 0.5 ADA - not enough for 10 ADA tx + fakeClient.setupWallet(senderAddress, balance) + + // When + val utxosResult = fakeClient.getUtxos(senderAddress) + + // Then + assertThat(utxosResult.isSuccess).isTrue() + val total = utxosResult.getOrNull()!!.sumOf { it.amount } + // Transaction builder would reject this as insufficient + assertThat(total).isLessThan(10_000_000L) + } + + @Test + fun `UTXO selection - no UTXOs available`() = runTest { + // Given: Address with no UTXOs + val emptyAddress = "addr_test1_empty_wallet" + + // When + val result = fakeClient.getUtxos(emptyAddress) + + // Then + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEmpty() + } + + // ==================== Fee Calculation Tests ==================== + + @Test + fun `fee calculation uses protocol parameters`() = runTest { + // Given + fakeClient.protocolParameters = fakeClient.protocolParameters.copy( + minFeeA = 44L, + minFeeB = 155381L, + ) + + // When + val params = fakeClient.getProtocolParameters().getOrNull()!! + + // Then + assertThat(params.minFeeA).isEqualTo(44L) + assertThat(params.minFeeB).isEqualTo(155381L) + // Fee formula: fee = minFeeA * txSize + minFeeB + // For ~300 byte tx: 44 * 300 + 155381 = 168,581 lovelace + } + + @Test + fun `getProtocolParameters call count tracked`() = runTest { + // When + fakeClient.getProtocolParameters() + fakeClient.getProtocolParameters() + + // Then + assertThat(fakeClient.getProtocolParametersCallCount).isEqualTo(2) + } + + @Test + fun `getProtocolParameters fails on network error`() = runTest { + // Given + fakeClient.shouldFailWithNetworkError = true + + // When + val result = fakeClient.getProtocolParameters() + + // Then + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(CardanoException.NetworkException::class.java) + } + + // ==================== Error Type Tests ==================== + + @Test + fun `InsufficientFundsException contains correct amounts`() { + // Given + val exception = CardanoException.InsufficientFundsException( + required = 15_000_000L, + available = 10_000_000L, + ) + + // Then + assertThat(exception.required).isEqualTo(15_000_000L) + assertThat(exception.available).isEqualTo(10_000_000L) + assertThat(exception.message).contains("15000000") + assertThat(exception.message).contains("10000000") + } + + @Test + fun `InvalidAddressException contains address`() { + // Given + val badAddress = "not_a_valid_cardano_address" + val exception = CardanoException.InvalidAddressException(badAddress) + + // Then + assertThat(exception.address).isEqualTo(badAddress) + assertThat(exception.message).contains(badAddress) + } + + // ==================== Helper Methods ==================== + + private fun createPaymentRequest(amountLovelace: Long) = PaymentRequest( + fromAddress = senderAddress, + toAddress = recipientAddress, + amountLovelace = amountLovelace, + sessionId = testSessionId, + ) + + private fun createUtxo( + txHash: String, + outputIndex: Int, + amount: Long, + ) = Utxo( + txHash = txHash.padEnd(64, '0'), + outputIndex = outputIndex, + amount = amount, + address = senderAddress, + ) +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationPresenterTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationPresenterTest.kt new file mode 100644 index 0000000000..befedb6377 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/payment/PaymentConfirmationPresenterTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wallet.api.ProtocolParameters +import io.element.android.features.wallet.impl.cardano.CardanoNetworkConfig +import io.element.android.features.wallet.impl.cardano.CardanoNetwork +import io.element.android.features.wallet.test.FakeCardanoClient +import org.junit.Test + +/** + * Unit tests for payment confirmation logic. + * Note: Full presenter tests with Compose/Molecule require more setup. + * These tests verify the core logic. + */ +class PaymentConfirmationPresenterTest { + + private val testRecipientAddress = "addr_test1qp2fg770ddmqxxduasjsas8rgimrhknmqjn43mj74g7ta2tjt0n5nh4t5xqf6lp5mwfpksj9csjg9s4kgfhvwj7m7dcq9qf7zj" + + @Test + fun `testnet is configured correctly`() { + // Verify we are on testnet as per Phase 1 requirements + assertThat(CardanoNetworkConfig.NETWORK).isEqualTo(CardanoNetwork.TESTNET) + } + + @Test + fun `address truncation works correctly`() { + // First 8 + ... + last 6 + val truncated = if (testRecipientAddress.length > 16) { + "${testRecipientAddress.take(8)}...${testRecipientAddress.takeLast(6)}" + } else { + testRecipientAddress + } + assertThat(truncated).contains("...") + } + + @Test + fun `protocol parameters provide fee info`() { + val cardanoClient = FakeCardanoClient() + val params = cardanoClient.protocolParameters + + assertThat(params.minFeeA).isGreaterThan(0) + assertThat(params.minFeeB).isGreaterThan(0) + assertThat(params.maxTxSize).isGreaterThan(0) + assertThat(params.utxoCostPerByte).isGreaterThan(0) + } + + @Test + fun `fee calculation uses protocol parameters`() { + // Typical fee formula: minFeeA * txSize + minFeeB + val params = ProtocolParameters( + minFeeA = 44L, + minFeeB = 155381L, + maxTxSize = 16384, + utxoCostPerByte = 4310L, + ) + + // Assuming a ~350 byte transaction + val estimatedTxSize = 350 + val calculatedFee = params.minFeeA * estimatedTxSize + params.minFeeB + assertThat(calculatedFee).isEqualTo(170781L) + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryPresenterTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryPresenterTest.kt new file mode 100644 index 0000000000..dd346f7874 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/payment/PaymentEntryPresenterTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wallet.impl.slash.ParsedPayCommand +import io.element.android.libraries.matrix.api.core.UserId +import org.junit.Test + +/** + * Unit tests for payment entry validation logic. + * Note: Full presenter tests with Compose/Molecule require more setup. + * These tests verify the core validation logic. + */ +class PaymentEntryPresenterTest { + + @Test + fun `ParsedPayCommand AmountOnly extracts amount correctly`() { + val command = ParsedPayCommand.AmountOnly( + amount = 10_000_000L, + isTestnet = true + ) + assertThat(command.amount).isEqualTo(10_000_000L) + assertThat(command.isTestnet).isTrue() + } + + @Test + fun `ParsedPayCommand WithAddressRecipient extracts all fields`() { + val testAddress = "addr_test1qp2fg770ddmqxxduasjsas8rgimrhknmqjn43mj74g7ta2tjt0n5nh4t5xqf6lp5mwfpksj9csjg9s4kgfhvwj7m7dcq9qf7zj" + val command = ParsedPayCommand.WithAddressRecipient( + amount = 5_000_000L, + address = testAddress, + isTestnet = true + ) + assertThat(command.amount).isEqualTo(5_000_000L) + assertThat(command.address).isEqualTo(testAddress) + assertThat(command.isTestnet).isTrue() + } + + @Test + fun `ParsedPayCommand WithMatrixRecipient extracts matrix user ID`() { + val matrixUserId = UserId("@jacob:sulkta.com") + val command = ParsedPayCommand.WithMatrixRecipient( + amount = 10_000_000L, + matrixUserId = matrixUserId, + isTestnet = true + ) + assertThat(command.matrixUserId).isEqualTo(matrixUserId) + } + + @Test + fun `testnet address validation - valid address`() { + val validAddress = "addr_test1qp2fg770ddmqxxduasjsas8rgimrhknmqjn43mj74g7ta2tjt0n5nh4t5xqf6lp5mwfpksj9csjg9s4kgfhvwj7m7dcq9qf7zj" + assertThat(validAddress.startsWith("addr_test1")).isTrue() + } + + @Test + fun `mainnet address validation - valid address`() { + val validAddress = "addr1qxck4vlrf4xkxs2m4wcn7hpq98aqspflj3tdx8ax9qk9qw8z" + assertThat(validAddress.startsWith("addr1")).isTrue() + } + + @Test + fun `amount validation - ADA to lovelace conversion`() { + val adaAmount = 10.5 + val lovelace = (adaAmount * 1_000_000).toLong() + assertThat(lovelace).isEqualTo(10_500_000L) + } + + @Test + fun `amount validation - minimum amount is 1 ADA`() { + val minLovelace = 1_000_000L // 1 ADA + assertThat(500_000L < minLovelace).isTrue() // 0.5 ADA is below minimum + assertThat(1_000_000L >= minLovelace).isTrue() // 1 ADA is valid + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressPresenterTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressPresenterTest.kt new file mode 100644 index 0000000000..6f930ac97c --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/payment/PaymentProgressPresenterTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.payment + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wallet.api.TxStatus +import io.element.android.features.wallet.impl.cardano.CardanoNetworkConfig +import io.element.android.features.wallet.test.FakeCardanoClient +import io.element.android.features.wallet.test.FakeTransactionBuilder +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** + * Unit tests for payment progress logic. + * Note: Full presenter tests with Compose/Molecule require more setup. + * These tests verify the core transaction submission logic. + */ +class PaymentProgressPresenterTest { + + private val testTxHash = "abc123def456789012345678901234567890123456789012345678901234" + private val testRecipientAddress = "addr_test1qp2fg770ddmqxxduasjsas8rgimrhknmqjn43mj74g7ta2tjt0n5nh4t5xqf6lp5mwfpksj9csjg9s4kgfhvwj7m7dcq9qf7zj" + private val testAmountLovelace = 10_000_000L + + @Test + fun `tx hash truncation works correctly`() { + val truncated = if (testTxHash.length > 16) { + "${testTxHash.take(8)}...${testTxHash.takeLast(6)}" + } else { + testTxHash + } + assertThat(truncated).isEqualTo("abc123de...901234") + } + + @Test + fun `explorer URL generated for testnet`() { + val explorerUrl = "https://preprod.cardanoscan.io/transaction/$testTxHash" + assertThat(explorerUrl).contains("preprod.cardanoscan.io") + assertThat(explorerUrl).contains(testTxHash) + } + + @Test + fun `explorer URL generated for mainnet`() { + val explorerUrl = "https://cardanoscan.io/transaction/$testTxHash" + assertThat(explorerUrl).contains("cardanoscan.io") + assertThat(explorerUrl).doesNotContain("preprod") + } + + @Test + fun `transaction builder can build successfully`() = runTest { + val txBuilder = FakeTransactionBuilder.success() + val request = io.element.android.features.wallet.api.PaymentRequest(sessionId = io.element.android.libraries.matrix.api.core.SessionId("@test:matrix.org"), + fromAddress = "addr_test1sender", + toAddress = testRecipientAddress, + amountLovelace = testAmountLovelace, + ) + + val result = txBuilder.buildAndSign(request) + + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()?.fee).isGreaterThan(0) + } + + @Test + fun `transaction builder reports insufficient funds`() = runTest { + val txBuilder = FakeTransactionBuilder() + txBuilder.givenInsufficientFunds(available = 5_000_000, required = 10_180_000) + + val request = io.element.android.features.wallet.api.PaymentRequest(sessionId = io.element.android.libraries.matrix.api.core.SessionId("@test:matrix.org"), + fromAddress = "addr_test1sender", + toAddress = testRecipientAddress, + amountLovelace = testAmountLovelace, + ) + + val result = txBuilder.buildAndSign(request) + + assertThat(result.isFailure).isTrue() + } + + @Test + fun `cardano client can submit transaction`() = runTest { + val cardanoClient = FakeCardanoClient() + + val result = cardanoClient.submitTx("fake_signed_tx_cbor") + + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isNotNull() + } + + @Test + fun `transaction status polling works`() = runTest { + val cardanoClient = FakeCardanoClient() + cardanoClient.confirmTransaction(testTxHash) + + val result = cardanoClient.getTxStatus(testTxHash) + + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEqualTo(TxStatus.CONFIRMED) + } + + @Test + fun `network config is testnet`() { + assertThat(CardanoNetworkConfig.EXPLORER_BASE_URL).contains("preprod") + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/seedphrase/SeedPhraseManagerTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/seedphrase/SeedPhraseManagerTest.kt new file mode 100644 index 0000000000..733dc66ca5 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/seedphrase/SeedPhraseManagerTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.seedphrase + +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +class SeedPhraseManagerTest { + + private lateinit var seedPhraseManager: SeedPhraseManager + + @Before + fun setUp() { + seedPhraseManager = DefaultSeedPhraseManager() + } + + @Test + fun `generateSeedPhrase creates 24 words by default`() { + val words = seedPhraseManager.generateSeedPhrase() + + assertThat(words).hasSize(24) + } + + @Test + fun `generateSeedPhrase creates valid BIP-39 mnemonic`() { + val words = seedPhraseManager.generateSeedPhrase() + + val result = seedPhraseManager.validate(words) + assertThat(result).isInstanceOf(SeedPhraseValidationResult.Valid::class.java) + } + + @Test + fun `generateSeedPhrase with 12 words creates valid mnemonic`() { + val words = seedPhraseManager.generateSeedPhrase(12) + + assertThat(words).hasSize(12) + val result = seedPhraseManager.validate(words) + assertThat(result).isInstanceOf(SeedPhraseValidationResult.Valid::class.java) + } + + @Test + fun `generateSeedPhrase with invalid word count throws`() { + try { + seedPhraseManager.generateSeedPhrase(13) + assertThat(false).isTrue() // Should not reach here + } catch (e: IllegalArgumentException) { + assertThat(e.message).contains("Invalid word count") + } + } + + @Test + fun `validate returns Valid for correct mnemonic`() { + // Known valid test mnemonic + val validMnemonic = listOf( + "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", + "abandon", "abandon", "abandon", "abandon", "abandon", "about" + ) + + val result = seedPhraseManager.validate(validMnemonic) + + assertThat(result).isInstanceOf(SeedPhraseValidationResult.Valid::class.java) + assertThat((result as SeedPhraseValidationResult.Valid).wordCount).isEqualTo(12) + } + + @Test + fun `validate returns Invalid for wrong word count`() { + val invalidMnemonic = listOf("abandon", "abandon", "abandon") + + val result = seedPhraseManager.validate(invalidMnemonic) + + assertThat(result).isInstanceOf(SeedPhraseValidationResult.Invalid::class.java) + assertThat((result as SeedPhraseValidationResult.Invalid).error).contains("word count") + } + + @Test + fun `validate returns Invalid for invalid words`() { + val invalidMnemonic = listOf( + "notaword", "abandon", "abandon", "abandon", "abandon", "abandon", + "abandon", "abandon", "abandon", "abandon", "abandon", "about" + ) + + val result = seedPhraseManager.validate(invalidMnemonic) + + assertThat(result).isInstanceOf(SeedPhraseValidationResult.Invalid::class.java) + assertThat((result as SeedPhraseValidationResult.Invalid).error).contains("notaword") + } + + @Test + fun `validate returns Invalid for bad checksum`() { + // Valid words but invalid checksum + val invalidMnemonic = listOf( + "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", + "abandon", "abandon", "abandon", "abandon", "abandon", "abandon" + ) + + val result = seedPhraseManager.validate(invalidMnemonic) + + assertThat(result).isInstanceOf(SeedPhraseValidationResult.Invalid::class.java) + assertThat((result as SeedPhraseValidationResult.Invalid).error).contains("checksum") + } + + @Test + fun `validate string input works`() { + val validMnemonic = "abandon abandon abandon abandon abandon abandon " + + "abandon abandon abandon abandon abandon about" + + val result = seedPhraseManager.validate(validMnemonic) + + assertThat(result).isInstanceOf(SeedPhraseValidationResult.Valid::class.java) + } + + @Test + fun `normalize handles extra whitespace`() { + val input = " abandon abandon abandon " + + val result = seedPhraseManager.normalize(input) + + assertThat(result).containsExactly("abandon", "abandon", "abandon") + } + + @Test + fun `normalize lowercases words`() { + val input = "ABANDON Abandon aBaNdOn" + + val result = seedPhraseManager.normalize(input) + + assertThat(result).containsExactly("abandon", "abandon", "abandon") + } + + @Test + fun `suggestWords returns matching words`() { + val suggestions = seedPhraseManager.suggestWords("aban") + + assertThat(suggestions).contains("abandon") + } + + @Test + fun `suggestWords respects limit`() { + val suggestions = seedPhraseManager.suggestWords("a", limit = 3) + + assertThat(suggestions).hasSize(3) + } + + @Test + fun `suggestWords returns empty for blank prefix`() { + val suggestions = seedPhraseManager.suggestWords("") + + assertThat(suggestions).isEmpty() + } + + @Test + fun `getWordlist returns non-empty list`() { + val wordlist = seedPhraseManager.getWordlist() + + assertThat(wordlist).isNotEmpty() + assertThat(wordlist).hasSize(2048) // BIP-39 standard + } + + @Test + fun `generated mnemonics are unique`() { + val mnemonic1 = seedPhraseManager.generateSeedPhrase() + val mnemonic2 = seedPhraseManager.generateSeedPhrase() + + assertThat(mnemonic1).isNotEqualTo(mnemonic2) + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/slash/SlashCommandParserTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/slash/SlashCommandParserTest.kt new file mode 100644 index 0000000000..ee2f0f5c59 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/slash/SlashCommandParserTest.kt @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.slash + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.UserId +import org.junit.Test + +class SlashCommandParserTest { + private val parser = SlashCommandParser() + + // ==================== Basic Pattern Tests ==================== + + @Test + fun `parse returns null for non-slash-command input`() { + assertThat(parser.parse("Hello world")).isNull() + assertThat(parser.parse("pay 10 ADA")).isNull() + assertThat(parser.parse("/send 10 ADA")).isNull() + assertThat(parser.parse("")).isNull() + assertThat(parser.parse(" ")).isNull() + } + + @Test + fun `parse empty pay command returns Empty`() { + val result = parser.parse("/pay") + assertThat(result).isEqualTo(ParsedPayCommand.Empty) + } + + @Test + fun `parse pay command with trailing whitespace returns Empty`() { + val result = parser.parse("/pay ") + assertThat(result).isEqualTo(ParsedPayCommand.Empty) + } + + @Test + fun `parse pay is case insensitive`() { + assertThat(parser.parse("/PAY")).isEqualTo(ParsedPayCommand.Empty) + assertThat(parser.parse("/Pay")).isEqualTo(ParsedPayCommand.Empty) + assertThat(parser.parse("/pAy")).isEqualTo(ParsedPayCommand.Empty) + } + + // ==================== Amount-Only Tests ==================== + + @Test + fun `parse pay with integer amount assumes ADA`() { + val result = parser.parse("/pay 10") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + val amountOnly = result as ParsedPayCommand.AmountOnly + assertThat(amountOnly.amount).isEqualTo(10_000_000L) // 10 ADA in lovelace + assertThat(amountOnly.isTestnet).isFalse() + } + + @Test + fun `parse pay with decimal amount converts correctly`() { + val result = parser.parse("/pay 10.5") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + val amountOnly = result as ParsedPayCommand.AmountOnly + assertThat(amountOnly.amount).isEqualTo(10_500_000L) // 10.5 ADA in lovelace + } + + @Test + fun `parse pay with small decimal amount`() { + val result = parser.parse("/pay 0.000001") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + val amountOnly = result as ParsedPayCommand.AmountOnly + assertThat(amountOnly.amount).isEqualTo(1L) // 1 lovelace + } + + @Test + fun `parse pay with ADA unit`() { + val result = parser.parse("/pay 100 ADA") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + val amountOnly = result as ParsedPayCommand.AmountOnly + assertThat(amountOnly.amount).isEqualTo(100_000_000L) + assertThat(amountOnly.isTestnet).isFalse() + } + + @Test + fun `parse pay with tADA unit sets testnet flag`() { + val result = parser.parse("/pay 100 tADA") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + val amountOnly = result as ParsedPayCommand.AmountOnly + assertThat(amountOnly.amount).isEqualTo(100_000_000L) + assertThat(amountOnly.isTestnet).isTrue() + } + + @Test + fun `parse pay with lovelace unit`() { + val result = parser.parse("/pay 1000000 lovelace") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + val amountOnly = result as ParsedPayCommand.AmountOnly + assertThat(amountOnly.amount).isEqualTo(1_000_000_000_000L) // parser treats amount as ADA + } + + @Test + fun `parse pay with large amount`() { + val result = parser.parse("/pay 1000000") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + val amountOnly = result as ParsedPayCommand.AmountOnly + assertThat(amountOnly.amount).isEqualTo(1_000_000_000_000L) // 1 million ADA + } + + // ==================== Matrix Recipient Tests ==================== + + @Test + fun `parse pay with matrix user recipient`() { + val result = parser.parse("/pay 10 ADA @jacob:sulkta.com") + assertThat(result).isInstanceOf(ParsedPayCommand.WithMatrixRecipient::class.java) + val withRecipient = result as ParsedPayCommand.WithMatrixRecipient + assertThat(withRecipient.amount).isEqualTo(10_000_000L) + assertThat(withRecipient.matrixUserId).isEqualTo(UserId("@jacob:sulkta.com")) + assertThat(withRecipient.isTestnet).isFalse() + } + + @Test + fun `parse pay with matrix user no unit assumes ADA`() { + val result = parser.parse("/pay 5 @user:matrix.org") + assertThat(result).isInstanceOf(ParsedPayCommand.WithMatrixRecipient::class.java) + val withRecipient = result as ParsedPayCommand.WithMatrixRecipient + assertThat(withRecipient.amount).isEqualTo(5_000_000L) + assertThat(withRecipient.matrixUserId).isEqualTo(UserId("@user:matrix.org")) + } + + @Test + fun `parse pay with complex matrix user id`() { + val result = parser.parse("/pay 1 ADA @user.name_123-test=foo:server.example.com") + assertThat(result).isInstanceOf(ParsedPayCommand.WithMatrixRecipient::class.java) + val withRecipient = result as ParsedPayCommand.WithMatrixRecipient + assertThat(withRecipient.matrixUserId).isEqualTo(UserId("@user.name_123-test=foo:server.example.com")) + } + + // ==================== Cardano Address Tests ==================== + + @Test + fun `parse pay with mainnet address`() { + val address = "addr1qxck2frmdpldfsvlnvl0jmnh74mw56yyj4t7xuwzjw37msjks6mj7r28gqzve7a3pqzjqq5xn5yxqknnhj9f5pcy4jwsy7f0cc" + val result = parser.parse("/pay 10 ADA $address") + assertThat(result).isInstanceOf(ParsedPayCommand.WithAddressRecipient::class.java) + val withRecipient = result as ParsedPayCommand.WithAddressRecipient + assertThat(withRecipient.amount).isEqualTo(10_000_000L) + assertThat(withRecipient.address).isEqualTo(address) + assertThat(withRecipient.isTestnet).isFalse() + } + + @Test + fun `parse pay with testnet address and tADA`() { + val address = "addr_test1qpq2y7g8s5v4w2vj3fwzgxm0n8k7j6h5g4f3d2s1a0z9x8w7v6u5t4r3e2w1q0" + val result = parser.parse("/pay 10 tADA $address") + assertThat(result).isInstanceOf(ParsedPayCommand.WithAddressRecipient::class.java) + val withRecipient = result as ParsedPayCommand.WithAddressRecipient + assertThat(withRecipient.amount).isEqualTo(10_000_000L) + assertThat(withRecipient.address).isEqualTo(address) + assertThat(withRecipient.isTestnet).isTrue() + } + + @Test + fun `parse pay with address no unit assumes ADA`() { + val address = "addr1qxck2frmdpldfsvlnvl0jmnh74mw56yyj4t7xuwzjw37msjks6mj7r28gqzve7a3pqzjqq5xn5yxqknnhj9f5pcy4jwsy7f0cc" + val result = parser.parse("/pay 25 $address") + assertThat(result).isInstanceOf(ParsedPayCommand.WithAddressRecipient::class.java) + val withRecipient = result as ParsedPayCommand.WithAddressRecipient + assertThat(withRecipient.amount).isEqualTo(25_000_000L) + } + + // ==================== Error Cases ==================== + + @Test + fun `parse pay with invalid amount returns error`() { + val result = parser.parse("/pay banana") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + val error = result as ParsedPayCommand.ParseError + assertThat(error.reason).contains("Invalid amount") + } + + @Test + fun `parse pay with negative amount returns error`() { + // Note: negative won't match the regex, so it's treated as invalid + val result = parser.parse("/pay -10 ADA") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + } + + @Test + fun `parse pay with zero amount returns error`() { + val result = parser.parse("/pay 0 ADA") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + val error = result as ParsedPayCommand.ParseError + assertThat(error.reason).contains("greater than zero") + } + + @Test + fun `parse pay with amount exceeding max supply returns error`() { + val result = parser.parse("/pay 999999999999999999 ADA") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + val error = result as ParsedPayCommand.ParseError + assertThat(error.reason).contains("maximum") + } + + @Test + fun `parse pay with too many decimal places returns error`() { + val result = parser.parse("/pay 10.12345678 ADA") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + val error = result as ParsedPayCommand.ParseError + assertThat(error.reason).contains("Invalid amount") + } + + @Test + fun `parse pay with invalid matrix user returns error`() { + val result = parser.parse("/pay 10 ADA @invaliduser") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + val error = result as ParsedPayCommand.ParseError + assertThat(error.reason).contains("Invalid Matrix user ID") + } + + @Test + fun `parse pay with invalid address prefix returns error`() { + val result = parser.parse("/pay 10 ADA invalidaddr123456789012345678901234567890123456789012345678901234567890") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + val error = result as ParsedPayCommand.ParseError + assertThat(error.reason).contains("Invalid Cardano address") + } + + @Test + fun `parse pay with short address returns error`() { + val result = parser.parse("/pay 10 ADA addr1short") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + val error = result as ParsedPayCommand.ParseError + assertThat(error.reason).contains("too short") + } + + @Test + fun `parse pay with network mismatch mainnet address tADA returns error`() { + val mainnetAddress = "addr1qxck2frmdpldfsvlnvl0jmnh74mw56yyj4t7xuwzjw37msjks6mj7r28gqzve7a3pqzjqq5xn5yxqknnhj9f5pcy4jwsy7f0cc" + val result = parser.parse("/pay 10 tADA $mainnetAddress") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + val error = result as ParsedPayCommand.ParseError + assertThat(error.reason).contains("Network mismatch") + } + + @Test + fun `parse pay with network mismatch testnet address ADA returns error`() { + val testnetAddress = "addr_test1qpq2y7g8s5v4w2vj3fwzgxm0n8k7j6h5g4f3d2s1a0z9x8w7v6u5t4r3e2w1q0" + val result = parser.parse("/pay 10 ADA $testnetAddress") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + val error = result as ParsedPayCommand.ParseError + assertThat(error.reason).contains("Network mismatch") + } + + @Test + fun `parse pay with too many arguments returns error`() { + val result = parser.parse("/pay 10 ADA @user:server extra garbage") + assertThat(result).isInstanceOf(ParsedPayCommand.ParseError::class.java) + val error = result as ParsedPayCommand.ParseError + assertThat(error.reason).contains("Too many arguments") + } + + // ==================== Edge Cases ==================== + + @Test + fun `parse pay with extra whitespace between tokens`() { + val result = parser.parse("/pay 10 ADA") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + val amountOnly = result as ParsedPayCommand.AmountOnly + assertThat(amountOnly.amount).isEqualTo(10_000_000L) + } + + @Test + fun `parse pay with leading whitespace`() { + val result = parser.parse(" /pay 10 ADA") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + } + + @Test + fun `parse pay with trailing whitespace`() { + val result = parser.parse("/pay 10 ADA ") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + } + + @Test + fun `parse pay exact 6 decimal places`() { + val result = parser.parse("/pay 1.123456 ADA") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + val amountOnly = result as ParsedPayCommand.AmountOnly + assertThat(amountOnly.amount).isEqualTo(1_123_456L) + } + + @Test + fun `parse pay unit is case insensitive`() { + assertThat(parser.parse("/pay 1 ada")).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + assertThat(parser.parse("/pay 1 Ada")).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + assertThat(parser.parse("/pay 1 ADA")).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + } + + // ==================== isPartialPayCommand Tests ==================== + + @Test + fun `isPartialPayCommand returns true for partial input`() { + assertThat(parser.isPartialPayCommand("/")).isTrue() + assertThat(parser.isPartialPayCommand("/p")).isTrue() + assertThat(parser.isPartialPayCommand("/pa")).isTrue() + assertThat(parser.isPartialPayCommand("/pay")).isTrue() + assertThat(parser.isPartialPayCommand("/pay ")).isTrue() + assertThat(parser.isPartialPayCommand("/pay 10")).isTrue() + } + + @Test + fun `isPartialPayCommand returns false for non-matching input`() { + assertThat(parser.isPartialPayCommand("")).isFalse() + assertThat(parser.isPartialPayCommand("pay")).isFalse() + assertThat(parser.isPartialPayCommand("/send")).isFalse() + assertThat(parser.isPartialPayCommand("/hello")).isFalse() + } + + // ==================== Real-World Usage Scenarios ==================== + + @Test + fun `scenario send 10 ADA to friend`() { + val result = parser.parse("/pay 10 ADA @friend:matrix.org") + assertThat(result).isInstanceOf(ParsedPayCommand.WithMatrixRecipient::class.java) + val cmd = result as ParsedPayCommand.WithMatrixRecipient + assertThat(cmd.amount).isEqualTo(10_000_000L) + assertThat(cmd.matrixUserId.value).isEqualTo("@friend:matrix.org") + } + + @Test + fun `scenario quick tip`() { + val result = parser.parse("/pay 1") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + assertThat((result as ParsedPayCommand.AmountOnly).amount).isEqualTo(1_000_000L) + } + + @Test + fun `scenario micropayment`() { + val result = parser.parse("/pay 0.5 ADA") + assertThat(result).isInstanceOf(ParsedPayCommand.AmountOnly::class.java) + assertThat((result as ParsedPayCommand.AmountOnly).amount).isEqualTo(500_000L) + } + + @Test + fun `scenario pay to external address`() { + val address = "addr1qxck2frmdpldfsvlnvl0jmnh74mw56yyj4t7xuwzjw37msjks6mj7r28gqzve7a3pqzjqq5xn5yxqknnhj9f5pcy4jwsy7f0cc" + val result = parser.parse("/pay 100 ADA $address") + assertThat(result).isInstanceOf(ParsedPayCommand.WithAddressRecipient::class.java) + val cmd = result as ParsedPayCommand.WithAddressRecipient + assertThat(cmd.amount).isEqualTo(100_000_000L) + assertThat(cmd.address).isEqualTo(address) + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemContentPaymentFactoryTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemContentPaymentFactoryTest.kt new file mode 100644 index 0000000000..d8a34846bb --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemContentPaymentFactoryTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.timeline + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wallet.api.PaymentCardStatus +import io.element.android.features.wallet.impl.payment.DefaultPaymentEventSender +import org.junit.Test + +class TimelineItemContentPaymentFactoryTest { + private val factory = TimelineItemContentPaymentFactory() + + @Test + fun `isPaymentMessage returns true for payment message prefix`() { + val message = "${DefaultPaymentEventSender.PAYMENT_MESSAGE_PREFIX}{\"amountLovelace\":1000000}" + assertThat(factory.isPaymentMessage(message)).isTrue() + } + + @Test + fun `isPaymentMessage returns false for other messages`() { + assertThat(factory.isPaymentMessage("Hello world")).isFalse() + assertThat(factory.isPaymentMessage("Some other message")).isFalse() + } + + @Test + fun `isStatusUpdateMessage returns true for status message prefix`() { + val message = "${DefaultPaymentEventSender.STATUS_MESSAGE_PREFIX}{\"status\":\"confirmed\"}" + assertThat(factory.isStatusUpdateMessage(message)).isTrue() + } + + @Test + fun `isStatusUpdateMessage returns false for other messages`() { + assertThat(factory.isStatusUpdateMessage("Hello world")).isFalse() + assertThat(factory.isStatusUpdateMessage("Payment message")).isFalse() + } + + @Test + fun `createFromRaw parses valid payment JSON`() { + val json = """{"amountLovelace":25000000,"toAddress":"addr1","fromAddress":"addr2","txHash":"abc123","status":"confirmed","network":"mainnet"}""" + + val result = factory.createFromRaw(json, isSentByMe = false) + + assertThat(result).isNotNull() + assertThat(result!!.amountLovelace).isEqualTo(25_000_000) + assertThat(result.amountAda).isEqualTo("25 ADA") + assertThat(result.toAddress).isEqualTo("addr1") + assertThat(result.fromAddress).isEqualTo("addr2") + assertThat(result.txHash).isEqualTo("abc123") + assertThat(result.status).isEqualTo(PaymentCardStatus.CONFIRMED) + assertThat(result.network).isEqualTo("mainnet") + assertThat(result.isTestnet).isFalse() + assertThat(result.isSentByMe).isFalse() + } + + @Test + fun `createFromRaw parses snake_case field names`() { + val json = """{"amount_lovelace":10000000,"to_address":"addr_test1abc","from_address":"addr_test1xyz","tx_hash":"hash123","status":"pending","network":"testnet"}""" + + val result = factory.createFromRaw(json, isSentByMe = true) + + assertThat(result).isNotNull() + assertThat(result!!.amountLovelace).isEqualTo(10_000_000) + assertThat(result.toAddress).isEqualTo("addr_test1abc") + assertThat(result.fromAddress).isEqualTo("addr_test1xyz") + assertThat(result.txHash).isEqualTo("hash123") + assertThat(result.status).isEqualTo(PaymentCardStatus.PENDING) + assertThat(result.network).isEqualTo("testnet") + assertThat(result.isSentByMe).isTrue() + } + + @Test + fun `createFromRaw parses wrapped event JSON with content field`() { + val json = """{"type":"co.sulkta.payment.request","content":{"amountLovelace":5000000,"toAddress":"addr","fromAddress":"addr2","txHash":"hash","status":"confirmed","network":"mainnet"}}""" + + val result = factory.createFromRaw(json, isSentByMe = false) + + assertThat(result).isNotNull() + assertThat(result!!.amountLovelace).isEqualTo(5_000_000) + assertThat(result.status).isEqualTo(PaymentCardStatus.CONFIRMED) + } + + @Test + fun `createFromRaw parses confirmed status`() { + val json = """{"amountLovelace":5000000,"toAddress":"addr","fromAddress":"addr2","txHash":"hash","status":"confirmed","network":"mainnet"}""" + + val result = factory.createFromRaw(json, isSentByMe = false) + + assertThat(result).isNotNull() + assertThat(result!!.status).isEqualTo(PaymentCardStatus.CONFIRMED) + } + + @Test + fun `createFromRaw parses failed status`() { + val json = """{"amountLovelace":1000000,"toAddress":"a","fromAddress":"b","txHash":null,"status":"failed","network":"testnet"}""" + + val result = factory.createFromRaw(json, isSentByMe = true) + + assertThat(result).isNotNull() + assertThat(result!!.status).isEqualTo(PaymentCardStatus.FAILED) + assertThat(result.txHash).isNull() + } + + @Test + fun `createFromRaw defaults to pending for unknown status`() { + val json = """{"amountLovelace":1000000,"toAddress":"a","fromAddress":"b","status":"unknown_status","network":"mainnet"}""" + + val result = factory.createFromRaw(json, isSentByMe = true) + + assertThat(result).isNotNull() + assertThat(result!!.status).isEqualTo(PaymentCardStatus.PENDING) + } + + @Test + fun `createFromRaw returns null for invalid JSON`() { + val json = "not valid json" + + val result = factory.createFromRaw(json, isSentByMe = true) + + assertThat(result).isNull() + } + + @Test + fun `createFromRaw returns null for missing required fields`() { + val json = """{"amountLovelace":1000000}""" + + val result = factory.createFromRaw(json, isSentByMe = true) + + assertThat(result).isNull() + } + + @Test + fun `createFromRaw returns null for empty JSON`() { + val json = "{}" + + val result = factory.createFromRaw(json, isSentByMe = true) + + assertThat(result).isNull() + } + + @Test + fun `createFromRaw formats fallback text correctly`() { + val json = """{"amountLovelace":1500000,"toAddress":"a","fromAddress":"b","status":"pending","network":"mainnet"}""" + + val result = factory.createFromRaw(json, isSentByMe = true) + + assertThat(result).isNotNull() + assertThat(result!!.fallbackText).isEqualTo("💰 1.5 ADA") + } +} diff --git a/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemPaymentContentTest.kt b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemPaymentContentTest.kt new file mode 100644 index 0000000000..623791c579 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/io/element/android/features/wallet/impl/timeline/TimelineItemPaymentContentTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.impl.timeline + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.wallet.api.PaymentCardStatus +import io.element.android.features.wallet.api.timeline.TimelineItemPaymentContent +import org.junit.Test + +class TimelineItemPaymentContentTest { + + @Test + fun `amountAda formats whole number correctly`() { + val content = createContent(amountLovelace = 10_000_000) + assertThat(content.amountAda).isEqualTo("10 ADA") + } + + @Test + fun `amountAda formats decimal correctly`() { + val content = createContent(amountLovelace = 5_500_000) + assertThat(content.amountAda).isEqualTo("5.5 ADA") + } + + @Test + fun `amountAda formats small amounts correctly`() { + val content = createContent(amountLovelace = 1_000) + assertThat(content.amountAda).isEqualTo("0.001 ADA") + } + + @Test + fun `amountAda formats zero correctly`() { + val content = createContent(amountLovelace = 0) + assertThat(content.amountAda).isEqualTo("0 ADA") + } + + @Test + fun `isTestnet returns true for testnet`() { + val content = createContent(network = "testnet") + assertThat(content.isTestnet).isTrue() + } + + @Test + fun `isTestnet returns true for preprod`() { + val content = createContent(network = "preprod") + assertThat(content.isTestnet).isTrue() + } + + @Test + fun `isTestnet returns true for preview`() { + val content = createContent(network = "preview") + assertThat(content.isTestnet).isTrue() + } + + @Test + fun `isTestnet returns false for mainnet`() { + val content = createContent(network = "mainnet") + assertThat(content.isTestnet).isFalse() + } + + @Test + fun `truncatedTxHash returns null when txHash is null`() { + val content = createContent(txHash = null) + assertThat(content.truncatedTxHash).isNull() + } + + @Test + fun `truncatedTxHash truncates long hash`() { + val content = createContent(txHash = "abc123def456789012345678901234567890xyz") + assertThat(content.truncatedTxHash).isEqualTo("abc123de...67890xyz") + } + + @Test + fun `truncatedTxHash keeps short hash intact`() { + val content = createContent(txHash = "shorthash") + assertThat(content.truncatedTxHash).isEqualTo("shorthash") + } + + @Test + fun `explorerUrl returns testnet URL for testnet`() { + val content = createContent(txHash = "abc123", network = "testnet") + assertThat(content.explorerUrl).isEqualTo("https://preprod.cardanoscan.io/transaction/abc123") + } + + @Test + fun `explorerUrl returns mainnet URL for mainnet`() { + val content = createContent(txHash = "abc123", network = "mainnet") + assertThat(content.explorerUrl).isEqualTo("https://cardanoscan.io/transaction/abc123") + } + + @Test + fun `explorerUrl returns null when txHash is null`() { + val content = createContent(txHash = null) + assertThat(content.explorerUrl).isNull() + } + + @Test + fun `type returns payment event type`() { + val content = createContent() + assertThat(content.type).isEqualTo("co.sulkta.payment.request") + } + + @Test + fun `formatAda companion function works correctly`() { + assertThat(TimelineItemPaymentContent.formatAda(1_000_000)).isEqualTo("1 ADA") + assertThat(TimelineItemPaymentContent.formatAda(1_500_000)).isEqualTo("1.5 ADA") + assertThat(TimelineItemPaymentContent.formatAda(100_000_000)).isEqualTo("100 ADA") + } + + private fun createContent( + amountLovelace: Long = 10_000_000, + toAddress: String = "addr_test1abc", + fromAddress: String = "addr_test1xyz", + txHash: String? = "hash123", + status: PaymentCardStatus = PaymentCardStatus.PENDING, + network: String = "testnet", + isSentByMe: Boolean = true, + fallbackText: String = "💰 Sent 10 ADA", + ) = TimelineItemPaymentContent( + amountLovelace = amountLovelace, + toAddress = toAddress, + fromAddress = fromAddress, + txHash = txHash, + status = status, + network = network, + isSentByMe = isSentByMe, + fallbackText = fallbackText, + ) +} diff --git a/features/wallet/test/build.gradle.kts b/features/wallet/test/build.gradle.kts new file mode 100644 index 0000000000..902aac9ebe --- /dev/null +++ b/features/wallet/test/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.wallet.test" +} + +dependencies { + api(projects.features.wallet.api) + api(projects.libraries.matrix.test) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.architecture) + implementation(projects.tests.testutils) + implementation(libs.coroutines.core) +} diff --git a/features/wallet/test/src/main/AndroidManifest.xml b/features/wallet/test/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0baf68a8a8 --- /dev/null +++ b/features/wallet/test/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + diff --git a/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakeCardanoClient.kt b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakeCardanoClient.kt new file mode 100644 index 0000000000..9cbd58a709 --- /dev/null +++ b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakeCardanoClient.kt @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.test + +import io.element.android.features.wallet.api.CardanoClient +import io.element.android.features.wallet.api.CardanoException +import io.element.android.features.wallet.api.NativeAsset +import io.element.android.features.wallet.api.NftMetadata +import io.element.android.features.wallet.api.ProtocolParameters +import io.element.android.features.wallet.api.TxStatus +import io.element.android.features.wallet.api.TxSummary +import io.element.android.features.wallet.api.Utxo +import io.element.android.features.wallet.api.UtxoAsset + +/** + * Fake implementation of [CardanoClient] for testing. + * + * Provides predictable test data and allows simulating various states: + * - Normal operation with configurable balances and UTxOs + * - Network errors + * - Rate limiting + * - Transaction lifecycle (pending → confirmed) + * - ADA Handle resolution + * - NFT metadata + */ +class FakeCardanoClient : CardanoClient { + // Configurable responses + var balances = mutableMapOf() + var utxos = mutableMapOf>() + var transactionStatuses = mutableMapOf() + var submittedTransactions = mutableListOf() + var assets = mutableMapOf>() + var transactions = mutableMapOf>() + var handles = mutableMapOf() // handle name (without $) -> address + var nftMetadata = mutableMapOf() // policyId+assetName -> metadata + + // Error simulation + var shouldFailWithNetworkError = false + var shouldFailWithRateLimit = false + var submitShouldFail = false + var submitErrorMessage: String? = null + var handleResolutionShouldFail = false + + // Protocol parameters (configurable) + var protocolParameters = ProtocolParameters( + minFeeA = 44L, + minFeeB = 155381L, + maxTxSize = 16384, + utxoCostPerByte = 4310L, + ) + + // Tracking for verification + var getBalanceCallCount = 0 + private set + var getUtxosCallCount = 0 + private set + var submitTxCallCount = 0 + private set + var getTxStatusCallCount = 0 + private set + var getProtocolParametersCallCount = 0 + private set + var getAddressAssetsCallCount = 0 + private set + var getAddressTransactionsCallCount = 0 + private set + var resolveHandleCallCount = 0 + private set + var getNftMetadataCallCount = 0 + private set + + /** + * Represents a submitted transaction for testing. + */ + data class SubmittedTx( + val cbor: String, + val generatedHash: String, + ) + + override suspend fun getBalance(address: String): Result { + getBalanceCallCount++ + + if (shouldFailWithNetworkError) { + return Result.failure(CardanoException.NetworkException("Simulated network error")) + } + if (shouldFailWithRateLimit) { + return Result.failure(CardanoException.RateLimitException(retryAfterMs = 1000L)) + } + + val balance = balances[address] ?: 0L + return Result.success(balance) + } + + override suspend fun getUtxos(address: String): Result> { + getUtxosCallCount++ + + if (shouldFailWithNetworkError) { + return Result.failure(CardanoException.NetworkException("Simulated network error")) + } + if (shouldFailWithRateLimit) { + return Result.failure(CardanoException.RateLimitException(retryAfterMs = 1000L)) + } + + val addressUtxos = utxos[address] ?: emptyList() + return Result.success(addressUtxos) + } + + override suspend fun submitTx(signedTxCbor: String): Result { + submitTxCallCount++ + + if (shouldFailWithNetworkError) { + return Result.failure(CardanoException.NetworkException("Simulated network error")) + } + if (shouldFailWithRateLimit) { + return Result.failure(CardanoException.RateLimitException(retryAfterMs = 1000L)) + } + if (submitShouldFail) { + return Result.failure( + CardanoException.SubmissionFailedException( + message = submitErrorMessage ?: "Simulated submission failure", + errorCode = "FAKE_ERROR", + ) + ) + } + + // Generate a fake tx hash + val txHash = "fake_tx_${System.currentTimeMillis()}_${submitTxCallCount}" + submittedTransactions.add(SubmittedTx(signedTxCbor, txHash)) + + // Auto-set to PENDING status + transactionStatuses[txHash] = TxStatus.PENDING + + return Result.success(txHash) + } + + override suspend fun getTxStatus(txHash: String): Result { + getTxStatusCallCount++ + + if (shouldFailWithNetworkError) { + return Result.failure(CardanoException.NetworkException("Simulated network error")) + } + if (shouldFailWithRateLimit) { + return Result.failure(CardanoException.RateLimitException(retryAfterMs = 1000L)) + } + + val status = transactionStatuses[txHash] ?: TxStatus.PENDING + return Result.success(status) + } + + override suspend fun getProtocolParameters(): Result { + getProtocolParametersCallCount++ + + if (shouldFailWithNetworkError) { + return Result.failure(CardanoException.NetworkException("Simulated network error")) + } + if (shouldFailWithRateLimit) { + return Result.failure(CardanoException.RateLimitException(retryAfterMs = 1000L)) + } + + return Result.success(protocolParameters) + } + + override suspend fun getAddressAssets(address: String): Result> { + getAddressAssetsCallCount++ + + if (shouldFailWithNetworkError) { + return Result.failure(CardanoException.NetworkException("Simulated network error")) + } + if (shouldFailWithRateLimit) { + return Result.failure(CardanoException.RateLimitException(retryAfterMs = 1000L)) + } + + return Result.success(assets[address] ?: emptyList()) + } + + override suspend fun getAddressTransactions(address: String, limit: Int): Result> { + getAddressTransactionsCallCount++ + + if (shouldFailWithNetworkError) { + return Result.failure(CardanoException.NetworkException("Simulated network error")) + } + if (shouldFailWithRateLimit) { + return Result.failure(CardanoException.RateLimitException(retryAfterMs = 1000L)) + } + + return Result.success(transactions[address]?.take(limit) ?: emptyList()) + } + + override suspend fun resolveHandle(handle: String): Result { + resolveHandleCallCount++ + + if (shouldFailWithNetworkError) { + return Result.failure(CardanoException.NetworkException("Simulated network error")) + } + if (shouldFailWithRateLimit) { + return Result.failure(CardanoException.RateLimitException(retryAfterMs = 1000L)) + } + if (handleResolutionShouldFail) { + return Result.failure(CardanoException.ApiException("Simulated handle resolution failure", "")) + } + + // Normalize to lowercase + val normalizedHandle = handle.lowercase().trim() + val address = handles[normalizedHandle] + return Result.success(address) + } + + override suspend fun getNftMetadata(policyId: String, assetName: String): Result { + getNftMetadataCallCount++ + + if (shouldFailWithNetworkError) { + return Result.failure(CardanoException.NetworkException("Simulated network error")) + } + if (shouldFailWithRateLimit) { + return Result.failure(CardanoException.RateLimitException(retryAfterMs = 1000L)) + } + + val key = "$policyId$assetName" + return Result.success(nftMetadata[key]) + } + + // Helper methods for test setup + + /** + * Sets up a test wallet with a given balance and UTxOs. + */ + fun setupWallet( + address: String, + balanceLovelace: Long, + utxoList: List = createDefaultUtxos(address, balanceLovelace), + ) { + balances[address] = balanceLovelace + utxos[address] = utxoList + } + + /** + * Simulates transaction confirmation. + */ + fun confirmTransaction(txHash: String) { + transactionStatuses[txHash] = TxStatus.CONFIRMED + } + + /** + * Simulates transaction failure. + */ + fun failTransaction(txHash: String) { + transactionStatuses[txHash] = TxStatus.FAILED + } + + /** + * Configures a specific balance for getBalance calls. + */ + fun givenBalance(balance: Long, address: String = TEST_ADDRESS) { + balances[address] = balance + } + + /** + * Configures the protocol parameters to return. + */ + fun givenProtocolParameters(params: ProtocolParameters) { + this.protocolParameters = params + } + + /** + * Configures submitTx to succeed with a specific hash. + */ + fun givenSubmitSuccess(txHash: String) { + submitShouldFail = false + // Override the generated hash by pre-setting status + transactionStatuses[txHash] = TxStatus.PENDING + } + + /** + * Configures submitTx to fail with a specific error. + */ + fun givenSubmitFailure(errorMessage: String) { + submitShouldFail = true + submitErrorMessage = errorMessage + } + + /** + * Configures an ADA Handle to resolve to a specific address. + */ + fun givenHandle(handle: String, address: String) { + handles[handle.lowercase().trim()] = address + } + + /** + * Configures NFT metadata for an asset. + */ + fun givenNftMetadata(policyId: String, assetName: String, metadata: NftMetadata) { + nftMetadata["$policyId$assetName"] = metadata + } + + /** + * Resets all state and counters. + */ + fun reset() { + balances.clear() + utxos.clear() + transactionStatuses.clear() + submittedTransactions.clear() + assets.clear() + transactions.clear() + handles.clear() + nftMetadata.clear() + shouldFailWithNetworkError = false + shouldFailWithRateLimit = false + submitShouldFail = false + submitErrorMessage = null + handleResolutionShouldFail = false + getBalanceCallCount = 0 + getUtxosCallCount = 0 + submitTxCallCount = 0 + getTxStatusCallCount = 0 + getProtocolParametersCallCount = 0 + getAddressAssetsCallCount = 0 + getAddressTransactionsCallCount = 0 + resolveHandleCallCount = 0 + getNftMetadataCallCount = 0 + protocolParameters = ProtocolParameters( + minFeeA = 44L, + minFeeB = 155381L, + maxTxSize = 16384, + utxoCostPerByte = 4310L, + ) + } + + companion object { + /** + * Creates a default set of UTxOs for testing. + * Splits the balance into multiple UTxOs for realistic scenarios. + */ + fun createDefaultUtxos( + address: String, + totalLovelace: Long, + assets: List = emptyList(), + ): List { + if (totalLovelace <= 0) return emptyList() + + // Create 2-3 UTxOs that sum to the total + val utxo1Amount = totalLovelace / 2 + val utxo2Amount = totalLovelace - utxo1Amount + + return listOf( + Utxo( + txHash = "aabbccdd11223344556677889900aabbccdd11223344556677889900aabbccdd", + outputIndex = 0, + amount = utxo1Amount, + address = address, + assets = assets, // First UTXO holds the assets + ), + Utxo( + txHash = "11223344556677889900aabbccdd11223344556677889900aabbccdd11223344", + outputIndex = 1, + amount = utxo2Amount, + address = address, + assets = emptyList(), + ), + ) + } + + /** + * A test address for testnet. + */ + const val TEST_ADDRESS = "addr_test1qpu5vlrf4xkxs2m4wcn7hpq98aqspflj3tdx8ax9qk9qw8zqh2c4tkqehp4j0y8awxmjcgv5p2vz8z5zycq7vq4q2dqst7pf8y" + + /** + * A test address for mainnet. + */ + const val MAINNET_ADDRESS = "addr1qxck4vlrf4xkxs2m4wcn7hpq98aqspflj3tdx8ax9qk9qw8zqh2c4tkqehp4j0y8awxmjcgv5p2vz8z5zycq7vq4q2dqsfxh8m3" + } +} diff --git a/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakePaymentEventSender.kt b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakePaymentEventSender.kt new file mode 100644 index 0000000000..c34341595a --- /dev/null +++ b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakePaymentEventSender.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.test + +import io.element.android.features.wallet.api.PaymentEventSender +import io.element.android.features.wallet.api.PaymentRequest +import io.element.android.features.wallet.api.SignedTransaction +import io.element.android.libraries.matrix.api.timeline.Timeline + +/** + * Fake implementation of [PaymentEventSender] for testing. + */ +class FakePaymentEventSender : PaymentEventSender { + var sentPayments = mutableListOf() + var sentStatusUpdates = mutableListOf() + var sendPaymentResult: Result = Result.success(Unit) + var sendStatusUpdateResult: Result = Result.success(Unit) + + override suspend fun sendPaymentEvent( + timeline: Timeline, + request: PaymentRequest, + signedTx: SignedTransaction, + network: String, + ): Result { + sentPayments.add( + SentPayment( + request = request, + signedTx = signedTx, + network = network, + ) + ) + return sendPaymentResult + } + + override suspend fun sendStatusUpdate( + timeline: Timeline, + txHash: String, + newStatus: String, + network: String, + ): Result { + sentStatusUpdates.add( + SentStatusUpdate( + txHash = txHash, + newStatus = newStatus, + network = network, + ) + ) + return sendStatusUpdateResult + } + + fun reset() { + sentPayments.clear() + sentStatusUpdates.clear() + sendPaymentResult = Result.success(Unit) + sendStatusUpdateResult = Result.success(Unit) + } + + data class SentPayment( + val request: PaymentRequest, + val signedTx: SignedTransaction, + val network: String, + ) + + data class SentStatusUpdate( + val txHash: String, + val newStatus: String, + val network: String, + ) +} diff --git a/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakePaymentStatusPoller.kt b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakePaymentStatusPoller.kt new file mode 100644 index 0000000000..1190e9f001 --- /dev/null +++ b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakePaymentStatusPoller.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.test + +import io.element.android.features.wallet.api.PaymentStatusPoller +import io.element.android.features.wallet.api.TxStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** + * Fake implementation of [PaymentStatusPoller] for testing. + * + * Allows configuring the sequence of statuses to emit for each transaction. + */ +class FakePaymentStatusPoller : PaymentStatusPoller { + + // Configurable status sequences per transaction + private val statusSequences = mutableMapOf>() + + // Default behavior: PENDING → CONFIRMED + var defaultSequence = listOf(TxStatus.PENDING, TxStatus.CONFIRMED) + + // Tracking + val polledTransactions = mutableListOf() + var pollCallCount = 0 + private set + + override fun pollUntilConfirmed(txHash: String): Flow = flow { + pollCallCount++ + polledTransactions.add(txHash) + + val sequence = statusSequences[txHash] ?: defaultSequence + for (status in sequence) { + emit(status) + } + } + + /** + * Configures the status sequence for a specific transaction. + */ + fun givenStatusSequence(txHash: String, vararg statuses: TxStatus) { + statusSequences[txHash] = statuses.toList() + } + + /** + * Configures a transaction to confirm immediately. + */ + fun givenConfirmsImmediately(txHash: String) { + statusSequences[txHash] = listOf(TxStatus.PENDING, TxStatus.CONFIRMED) + } + + /** + * Configures a transaction to fail. + */ + fun givenFails(txHash: String) { + statusSequences[txHash] = listOf(TxStatus.PENDING, TxStatus.FAILED) + } + + /** + * Configures a transaction to stay pending indefinitely. + */ + fun givenStaysPending(txHash: String) { + statusSequences[txHash] = listOf(TxStatus.PENDING) + } + + /** + * Resets all state. + */ + fun reset() { + statusSequences.clear() + polledTransactions.clear() + pollCallCount = 0 + defaultSequence = listOf(TxStatus.PENDING, TxStatus.CONFIRMED) + } +} diff --git a/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakeTransactionBuilder.kt b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakeTransactionBuilder.kt new file mode 100644 index 0000000000..789ea57452 --- /dev/null +++ b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakeTransactionBuilder.kt @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.test + +import io.element.android.features.wallet.api.CardanoException +import io.element.android.features.wallet.api.PaymentRequest +import io.element.android.features.wallet.api.SignedTransaction +import io.element.android.features.wallet.api.TransactionBuilder + +/** + * Fake implementation of [TransactionBuilder] for testing. + * + * Provides configurable success/failure responses and tracks calls + * for assertion in presenter tests. + */ +class FakeTransactionBuilder : TransactionBuilder { + + // Configurable responses + var nextResult: Result? = null + var shouldSucceed = true + var errorToThrow: CardanoException? = null + + // Default successful response + var defaultFee = 180_000L + var defaultTxHashPrefix = "fake_tx_" + + // Tracking for verification + val buildAndSignCalls = mutableListOf() + var buildAndSignCallCount = 0 + private set + + override suspend fun buildAndSign(request: PaymentRequest): Result { + buildAndSignCallCount++ + buildAndSignCalls.add(request) + + // Return configured result if set + nextResult?.let { return it } + + // Return error if configured + errorToThrow?.let { return Result.failure(it) } + + // Return failure if shouldSucceed is false + if (!shouldSucceed) { + return Result.failure( + CardanoException.ApiException( + message = "Simulated build failure", + response = "FAKE_ERROR" + ) + ) + } + + // Return successful transaction + val txHash = "$defaultTxHashPrefix${System.currentTimeMillis()}_$buildAndSignCallCount" + return Result.success( + SignedTransaction( + txCbor = generateFakeCbor(request), + txHash = txHash, + fee = defaultFee, + actualAmount = request.amountLovelace, + ) + ) + } + + /** + * Configures the builder to return a successful transaction. + */ + fun givenSuccess(fee: Long = defaultFee) { + shouldSucceed = true + errorToThrow = null + nextResult = null + defaultFee = fee + } + + /** + * Configures the builder to fail with insufficient funds error. + */ + fun givenInsufficientFunds(available: Long, required: Long) { + errorToThrow = CardanoException.InsufficientFundsException( + required = required, + available = available + ) + nextResult = null + } + + /** + * Configures the builder to fail with invalid address error. + */ + fun givenInvalidAddress(address: String) { + errorToThrow = CardanoException.InvalidAddressException(address) + nextResult = null + } + + /** + * Configures the builder to fail with a network error. + */ + fun givenNetworkError(message: String = "Network error") { + errorToThrow = CardanoException.NetworkException(message) + nextResult = null + } + + /** + * Configures the builder to return a specific result. + */ + fun givenResult(result: Result) { + nextResult = result + errorToThrow = null + } + + /** + * Gets the most recent build request, if any. + */ + fun getLastRequest(): PaymentRequest? = buildAndSignCalls.lastOrNull() + + /** + * Verifies that buildAndSign was called with specific parameters. + */ + fun verifyBuildAndSignCalled( + fromAddress: String? = null, + toAddress: String? = null, + amountLovelace: Long? = null, + ): Boolean { + return buildAndSignCalls.any { request -> + (fromAddress == null || request.fromAddress == fromAddress) && + (toAddress == null || request.toAddress == toAddress) && + (amountLovelace == null || request.amountLovelace == amountLovelace) + } + } + + /** + * Resets all state and counters. + */ + fun reset() { + nextResult = null + shouldSucceed = true + errorToThrow = null + defaultFee = 180_000L + buildAndSignCalls.clear() + buildAndSignCallCount = 0 + } + + /** + * Generates fake CBOR data for testing. + */ + private fun generateFakeCbor(request: PaymentRequest): String { + // Generate a predictable fake CBOR hex string + // In real implementation this would be actual CBOR + val seed = request.hashCode() + return buildString { + repeat(200) { + append("%02x".format((seed + it) and 0xFF)) + } + } + } + + companion object { + /** Creates a FakeTransactionBuilder configured for success */ + fun success(fee: Long = 180_000L) = FakeTransactionBuilder().apply { + givenSuccess(fee) + } + + /** Creates a FakeTransactionBuilder configured to fail with insufficient funds */ + fun insufficientFunds(available: Long, required: Long) = FakeTransactionBuilder().apply { + givenInsufficientFunds(available, required) + } + } +} diff --git a/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakeWalletEntryPoint.kt b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakeWalletEntryPoint.kt new file mode 100644 index 0000000000..fc08550a1e --- /dev/null +++ b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/FakeWalletEntryPoint.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.test + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.features.wallet.api.WalletEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId + +class FakeWalletEntryPoint : WalletEntryPoint { + class Builder : WalletEntryPoint.Builder { + override fun setRoomId(roomId: RoomId): WalletEntryPoint.Builder = this + override fun setRecipientUserId(userId: UserId?): WalletEntryPoint.Builder = this + override fun setRecipientAddress(address: String?): WalletEntryPoint.Builder = this + override fun setAmount(amount: String?): WalletEntryPoint.Builder = this + + override fun build(): Node { + throw NotImplementedError("FakeWalletEntryPoint cannot build a real node") + } + } + + override fun paymentFlowBuilder( + parentNode: Node, + buildContext: BuildContext, + callback: WalletEntryPoint.Callback, + ): WalletEntryPoint.Builder = Builder() +} diff --git a/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/storage/FakeCardanoKeyStorage.kt b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/storage/FakeCardanoKeyStorage.kt new file mode 100644 index 0000000000..325f36982c --- /dev/null +++ b/features/wallet/test/src/main/kotlin/io/element/android/features/wallet/test/storage/FakeCardanoKeyStorage.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.features.wallet.test.storage + +import io.element.android.features.wallet.api.storage.CardanoKeyStorage +import io.element.android.features.wallet.api.storage.WalletCreationResult +import io.element.android.libraries.matrix.api.core.SessionId + +/** + * Fake implementation of [CardanoKeyStorage] for testing. + */ +class FakeCardanoKeyStorage : CardanoKeyStorage { + private val wallets = mutableMapOf() + + var generateWalletResult: Result = Result.success( + WalletCreationResult( + mnemonic = List(24) { "word$it" }, + baseAddress = "addr_test1qpfake", + stakeAddress = "stake_test1upfake", + ) + ) + + var importWalletResult: Result = Result.success("addr_test1qpimported") + var getMnemonicResult: Result>? = null + var getBaseAddressResult: Result? = null + var getStakeAddressResult: Result? = null + var deleteWalletResult: Result = Result.success(Unit) + + private data class WalletData( + val mnemonic: List, + val baseAddress: String, + val stakeAddress: String, + ) + + override suspend fun hasWallet(sessionId: SessionId): Boolean { + return wallets.containsKey(sessionId.value) + } + + override suspend fun generateWallet(sessionId: SessionId): Result { + return generateWalletResult.onSuccess { result -> + if (wallets.containsKey(sessionId.value)) { + return Result.failure(IllegalStateException("Wallet already exists")) + } + wallets[sessionId.value] = WalletData( + mnemonic = result.mnemonic, + baseAddress = result.baseAddress, + stakeAddress = result.stakeAddress, + ) + } + } + + override suspend fun importWallet(sessionId: SessionId, mnemonic: List): Result { + return importWalletResult.onSuccess { address -> + if (wallets.containsKey(sessionId.value)) { + return Result.failure(IllegalStateException("Wallet already exists")) + } + wallets[sessionId.value] = WalletData( + mnemonic = mnemonic, + baseAddress = address, + stakeAddress = "stake_test1upimported", + ) + } + } + + override suspend fun getMnemonic(sessionId: SessionId): Result> { + getMnemonicResult?.let { return it } + val wallet = wallets[sessionId.value] + ?: return Result.failure(IllegalStateException("No wallet")) + return Result.success(wallet.mnemonic) + } + + override suspend fun getBaseAddress(sessionId: SessionId, addressIndex: Int): Result { + getBaseAddressResult?.let { return it } + val wallet = wallets[sessionId.value] + ?: return Result.failure(IllegalStateException("No wallet")) + return Result.success(wallet.baseAddress) + } + + override suspend fun getStakeAddress(sessionId: SessionId): Result { + getStakeAddressResult?.let { return it } + val wallet = wallets[sessionId.value] + ?: return Result.failure(IllegalStateException("No wallet")) + return Result.success(wallet.stakeAddress) + } + + override suspend fun deleteWallet(sessionId: SessionId): Result { + wallets.remove(sessionId.value) + return deleteWalletResult + } + + fun reset() { + wallets.clear() + generateWalletResult = Result.success( + WalletCreationResult( + mnemonic = List(24) { "word$it" }, + baseAddress = "addr_test1qpfake", + stakeAddress = "stake_test1upfake", + ) + ) + importWalletResult = Result.success("addr_test1qpimported") + getMnemonicResult = null + getBaseAddressResult = null + getStakeAddressResult = null + deleteWalletResult = Result.success(Unit) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a328218d79..63428fd6f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,9 +5,9 @@ # Project android_gradle_plugin = "8.13.2" # When updating this, please also update the version in the file ./idea/kotlinc.xml -kotlin = "2.3.21" +kotlin = "2.3.20" kotlinpoet = "2.3.0" -ksp = "2.3.8" +ksp = "2.3.6" firebaseAppDistribution = "5.2.1" # AndroidX @@ -17,26 +17,26 @@ constraintlayout = "2.2.1" constraintlayout_compose = "1.1.1" lifecycle = "2.10.0" activity = "1.13.0" -media3 = "1.10.1" -camera = "1.6.1" +media3 = "1.10.0" +camera = "1.5.3" work = "2.11.2" # Compose -compose_bom = "2026.05.00" +compose_bom = "2026.03.01" # Coroutines -coroutines = "1.11.0" +coroutines = "1.10.2" # Accompanist accompanist = "0.37.3" # Test test_core = "1.7.0" -roborazzi = "1.60.0" +roborazzi = "1.59.0" # Jetbrain -datetime = "0.8.0" -serialization_json = "1.11.0" +datetime = "0.7.1" +serialization_json = "1.10.0" #other coil = "3.4.0" @@ -51,10 +51,10 @@ telephoto = "0.19.0" haze = "1.7.2" # Dependency analysis -dependencyAnalysis = "3.12.0" +dependencyAnalysis = "3.6.1" # DI -metro = "1.1.1" +metro = "0.13.2" # Auto service autoservice = "1.1.1" @@ -80,7 +80,7 @@ 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.13.0" +google_firebase_bom = "com.google.firebase:firebase-bom:34.11.0" firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" } autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" } ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } @@ -111,14 +111,13 @@ 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.16.0" +androidx_webkit = "androidx.webkit:webkit:1.15.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' } @@ -169,7 +168,7 @@ 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.9.0" +test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.8.2" test_detekt_api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" } test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" } @@ -179,7 +178,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # All new features should not be implemented in the pull request that upgrades the version, developers should # only fix API breaks and may add some TODOs. -matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.05.20" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.04.15" # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } @@ -192,7 +191,7 @@ serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.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.22.2" +jsoup = "org.jsoup:jsoup:1.21.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" @@ -201,29 +200,28 @@ 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.16.0" +sqlcipher = "net.zetetic:sqlcipher-android:4.14.1" sqlite = "androidx.sqlite:sqlite-ktx:2.6.2" unifiedpush = "org.unifiedpush.android:connector:3.3.2" vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0" telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-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.1.0" +maplibre = "org.maplibre.gl:android-sdk:13.0.2" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2" maplibre_compose = "org.maplibre.compose:maplibre-compose:0.12.1" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2" 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.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.43.0" -sentry = "io.sentry:sentry-android:8.41.0" +posthog = "com.posthog:posthog-android:3.39.0" +sentry = "io.sentry:sentry-android:8.37.1" # main branch can be tested replacing the version with main-SNAPSHOT matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.33.2" @@ -235,7 +233,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.4" +element_call_embedded = "io.element.android:element-call-embedded:0.19.1" # Auto services google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } @@ -263,13 +261,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.2" +dependencycheck = "org.owasp.dependencycheck:12.2.0" dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" } paparazzi = "app.cash.paparazzi:2.0.0-alpha04" 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.3.0.8198" +sonarqube = "org.sonarqube:7.2.3.7755" 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/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 12f2957678..1e25599962 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,13 +23,6 @@ fun interface JsonProvider { @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) class DefaultJsonProvider : JsonProvider { - private val json: Json by lazy { - Json { - ignoreUnknownKeys = true - allowComments = true - allowTrailingComma = true - } - } - + private val json: Json by lazy { Json { ignoreUnknownKeys = true } } override fun invoke() = json } diff --git a/libraries/androidutils/src/main/res/values-ca/translations.xml b/libraries/androidutils/src/main/res/values-ca/translations.xml deleted file mode 100644 index aba099db1f..0000000000 --- a/libraries/androidutils/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - "No s\'ha trobat cap aplicació compatible per gestionar aquesta acció." - 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 da849b753e..ce89e8a9d9 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,6 +11,7 @@ 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 @@ -87,9 +88,11 @@ 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/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt b/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt index bc6a42d2f9..faab73593e 100644 --- a/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt +++ b/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt @@ -19,7 +19,6 @@ import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.audio.api.AudioFocusRequester import io.element.android.libraries.di.annotations.ApplicationContext -import timber.log.Timber @ContributesBinding(AppScope::class) class DefaultAudioFocus( @@ -39,11 +38,9 @@ class DefaultAudioFocus( when (it) { AudioManager.AUDIOFOCUS_GAIN -> { // Do nothing - Timber.d("AudioFocus: AUDIOFOCUS_GAIN") } AudioManager.AUDIOFOCUS_LOSS -> { // Permanent focus loss (e.g., phone call) — always stop/pause. - Timber.d("AudioFocus: AUDIOFOCUS_LOSS") onFocusLost() } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, diff --git a/libraries/cachestore/api/build.gradle.kts b/libraries/cachestore/api/build.gradle.kts deleted file mode 100644 index 0e03bb5136..0000000000 --- a/libraries/cachestore/api/build.gradle.kts +++ /dev/null @@ -1,13 +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. - */ -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 deleted file mode 100644 index a448ba7df8..0000000000 --- a/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheData.kt +++ /dev/null @@ -1,13 +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.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 deleted file mode 100644 index 5df446f688..0000000000 --- a/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.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 deleted file mode 100644 index f0c7ba237c..0000000000 --- a/libraries/cachestore/impl/build.gradle.kts +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 2a33ce4fda..0000000000 --- a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt +++ /dev/null @@ -1,26 +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.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 deleted file mode 100644 index 54766803f9..0000000000 --- a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt +++ /dev/null @@ -1,40 +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.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 deleted file mode 100644 index 05fa3d9d97..0000000000 --- a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt +++ /dev/null @@ -1,43 +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.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.RandomSecretPassphraseProvider - -@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 passphraseProvider = RandomSecretPassphraseProvider(context, secretFile) - val driver = SqlCipherDriverFactory(passphraseProvider) - .create(CacheDatabase.Schema, "$name.db", context) - 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 deleted file mode 100644 index 8e4b0cac72..0000000000 Binary files a/libraries/cachestore/impl/src/main/sqldelight/databases/1.db and /dev/null 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 deleted file mode 100644 index fd350ac7ba..0000000000 --- a/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq +++ /dev/null @@ -1,28 +0,0 @@ --------------------------------------------------------------------- --- 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/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 deleted file mode 100644 index 36d7e05532..0000000000 --- a/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt +++ /dev/null @@ -1,86 +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.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 deleted file mode 100644 index 3dca9efaf3..0000000000 --- a/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt +++ /dev/null @@ -1,21 +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.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 deleted file mode 100644 index 7ad2ac48d9..0000000000 --- a/libraries/cachestore/test/build.gradle.kts +++ /dev/null @@ -1,17 +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. - */ -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 deleted file mode 100644 index 30633e8ff9..0000000000 --- a/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/CacheData.kt +++ /dev/null @@ -1,18 +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.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 deleted file mode 100644 index f029c8bcde..0000000000 --- a/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt +++ /dev/null @@ -1,33 +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.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/MaterialText Colors.png b/libraries/compound/screenshots/MaterialText Colors.png index 1273aee01e..f8f77ccca2 100644 --- a/libraries/compound/screenshots/MaterialText Colors.png +++ b/libraries/compound/screenshots/MaterialText Colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f1a277e76d351f48ae0041e082525422604fbf41d77fe078112349855dd3d2e -size 453512 +oid sha256:4be10c3bb9900d27a3b406eca0cb902b0ff9cdf90e8e3cf1ae7760aa7c5d47d9 +size 377446 diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/previews/CompoundIconsPreview.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/CompoundIconsPreview.kt index 7b1219a9f9..236e3627f4 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/previews/CompoundIconsPreview.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/CompoundIconsPreview.kt @@ -38,7 +38,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.theme.Theme import io.element.android.compound.tokens.generated.CompoundIcons import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -63,7 +62,7 @@ internal fun IconsCompoundPreviewRtl() = ElementTheme { @Preview(widthDp = 730, heightDp = 1920) @Composable -internal fun IconsCompoundPreviewDark() = ElementTheme(theme = Theme.Dark) { +internal fun IconsCompoundPreviewDark() = ElementTheme(darkTheme = true) { IconsCompoundPreview() } diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/previews/SemanticColorsPreview.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/SemanticColorsPreview.kt index 6b1e77676c..ff7970c0f4 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/previews/SemanticColorsPreview.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/previews/SemanticColorsPreview.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.theme.Theme import io.element.android.compound.tokens.generated.compoundColorsHcDark import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf @@ -66,7 +65,7 @@ internal fun CompoundSemanticColorsLightHc() = ElementTheme( @Preview(heightDp = 2000) @Composable -internal fun CompoundSemanticColorsDark() = ElementTheme(theme = Theme.Dark) { +internal fun CompoundSemanticColorsDark() = ElementTheme(darkTheme = true) { Surface { Column( modifier = Modifier.padding(16.dp), @@ -86,7 +85,7 @@ internal fun CompoundSemanticColorsDark() = ElementTheme(theme = Theme.Dark) { @Preview(heightDp = 2000) @Composable internal fun CompoundSemanticColorsDarkHc() = ElementTheme( - theme = Theme.Dark, + darkTheme = true, compoundDark = compoundColorsHcDark, ) { Surface { diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/AvatarColors.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/AvatarColors.kt index 8d3e495332..763f422a00 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/AvatarColors.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/AvatarColors.kt @@ -65,7 +65,7 @@ internal fun AvatarColorsPreviewLight() { @Preview @Composable internal fun AvatarColorsPreviewDark() { - ElementTheme(theme = Theme.Dark) { + ElementTheme(darkTheme = true) { val chunks = avatarColors().chunked(4) Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { for (chunk in chunks) { diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt index ac83365658..bb2ae2b62e 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ElementTheme.kt @@ -77,10 +77,10 @@ internal val LocalCompoundColors = staticCompositionLocalOf { compoundColorsLigh /** * Sets up the theme for the application, or a part of it. * - * @param theme the [Theme] to use. Defaults to [Theme.Dark] or [Theme.Light] based on the system setting. + * @param darkTheme whether to use the dark theme or not. If `true`, the dark theme will be used. * @param applySystemBarsUpdate whether to update the system bars color scheme or not when the theme changes. It's `true` by default. * This is specially useful when you want to apply an alternate theme to a part of the app but don't want it to affect the system bars. - * @param lightStatusBar whether to use a light status bar color scheme or not. By default, it's `true` for light themes and `false` for dark ones. + * @param lightStatusBar whether to use a light status bar color scheme or not. By default, it's the opposite of [darkTheme]. * @param dynamicColor whether to enable MaterialYou or not. It's `false` by default. * @param compoundLight the [SemanticColors] to use in light theme. * @param compoundDark the [SemanticColors] to use in dark theme. @@ -91,9 +91,9 @@ internal val LocalCompoundColors = staticCompositionLocalOf { compoundColorsLigh */ @Composable fun ElementTheme( - theme: Theme = if (isSystemInDarkTheme()) Theme.Dark else Theme.Light, + darkTheme: Boolean = isSystemInDarkTheme(), applySystemBarsUpdate: Boolean = true, - lightStatusBar: Boolean = !theme.isDark(), + lightStatusBar: Boolean = !darkTheme, // true to enable MaterialYou dynamicColor: Boolean = false, compoundLight: SemanticColors = compoundColorsLight, @@ -103,13 +103,8 @@ fun ElementTheme( typography: Typography = compoundTypography, content: @Composable () -> Unit, ) { - val darkTheme = theme.isDark() val currentCompoundColor = when { - darkTheme -> if (theme == Theme.Black) { - compoundDark.copy(bgCanvasDefault = Color.Black) - } else { - compoundDark - } + darkTheme -> compoundDark else -> compoundLight } @@ -118,11 +113,7 @@ fun ElementTheme( val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> if (theme == Theme.Black) { - currentCompoundColor.toMaterialColorScheme() - } else { - materialColorsDark - } + darkTheme -> materialColorsDark else -> materialColorsLight } @@ -139,7 +130,7 @@ fun ElementTheme( if (applySystemBarsUpdate) { val activity = LocalActivity.current as? ComponentActivity - LaunchedEffect(statusBarColorScheme, theme, lightStatusBar) { + LaunchedEffect(statusBarColorScheme, darkTheme, lightStatusBar) { activity?.enableEdgeToEdge( // For Status bar use the background color of the app statusBarStyle = SystemBarStyle.auto( diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ForcedDarkElementTheme.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ForcedDarkElementTheme.kt index 8f9d2bdf0c..272245f199 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ForcedDarkElementTheme.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/ForcedDarkElementTheme.kt @@ -51,7 +51,7 @@ fun ForcedDarkElementTheme( } } ElementTheme( - theme = Theme.Dark, + darkTheme = true, compoundLight = colors.light, compoundDark = colors.dark, lightStatusBar = lightStatusBar, diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialTextPreview.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialTextPreview.kt index 27e874c141..792c7fbabf 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialTextPreview.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialTextPreview.kt @@ -36,15 +36,11 @@ internal fun MaterialTextPreview() = Row( ) { MaterialPreview( modifier = Modifier.weight(1f), - theme = Theme.Light, + darkTheme = false, ) MaterialPreview( modifier = Modifier.weight(1f), - theme = Theme.Dark, - ) - MaterialPreview( - modifier = Modifier.weight(1f), - theme = Theme.Black, + darkTheme = true, ) } @@ -56,7 +52,7 @@ private data class Model( @Composable private fun MaterialPreview( - theme: Theme, + darkTheme: Boolean, modifier: Modifier = Modifier, ) = Column(modifier = modifier) { Text( @@ -64,13 +60,13 @@ private fun MaterialPreview( .fillMaxWidth() .padding(8.dp), textAlign = TextAlign.Center, - text = theme.name, + text = if (darkTheme) "Dark" else "Light", color = Color.Black, fontSize = 18.sp, fontWeight = FontWeight.Bold, ) ElementTheme( - theme = theme, + darkTheme = darkTheme, ) { Column( modifier = Modifier.fillMaxSize() diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialThemeColors.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialThemeColors.kt index c283780295..c2a923e926 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialThemeColors.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/MaterialThemeColors.kt @@ -50,7 +50,7 @@ internal fun ColorsSchemeLightHcPreview() = ElementTheme( @Preview(heightDp = 1200) @Composable internal fun ColorsSchemeDarkPreview() = ElementTheme( - theme = Theme.Dark, + darkTheme = true, ) { ColorsSchemePreview( Color.White, @@ -62,7 +62,7 @@ internal fun ColorsSchemeDarkPreview() = ElementTheme( @Preview(heightDp = 1200) @Composable internal fun ColorsSchemeDarkHcPreview() = ElementTheme( - theme = Theme.Dark, + darkTheme = true, compoundDark = compoundColorsHcDark, ) { ColorsSchemePreview( @@ -71,15 +71,3 @@ internal fun ColorsSchemeDarkHcPreview() = ElementTheme( ElementTheme.materialColors, ) } - -@Preview(heightDp = 1200) -@Composable -internal fun ColorsSchemeBlackPreview() = ElementTheme( - theme = Theme.Black -) { - ColorsSchemePreview( - Color.White, - Color.Black, - ElementTheme.materialColors, - ) -} diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt index bf5932da59..131b14430c 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt @@ -16,26 +16,21 @@ import kotlinx.coroutines.flow.map enum class Theme { System, Dark, - Black, Light, } -private fun Theme.coerceBlackTheme(allowBlackTheme: Boolean): Theme { - return if (this == Theme.Black && !allowBlackTheme) Theme.Dark else this -} - @Composable fun Theme.isDark(): Boolean { return when (this) { Theme.System -> isSystemInDarkTheme() - Theme.Dark, Theme.Black -> true + Theme.Dark -> true Theme.Light -> false } } -fun Flow.mapToTheme(allowBlackTheme: Boolean): Flow = map { +fun Flow.mapToTheme(): Flow = map { when (it) { null -> Theme.System else -> Theme.valueOf(it) - }.coerceBlackTheme(allowBlackTheme) + } } 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..6e15233013 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 @@ -19,7 +19,6 @@ import io.element.android.compound.previews.IconsCompoundPreviewRtl import io.element.android.compound.previews.IconsPreview 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 kotlinx.collections.immutable.toImmutableList import org.junit.Test @@ -57,7 +56,7 @@ class CompoundIconTest { val content: List<@Composable ColumnScope.() -> Unit> = CompoundIcons.all.map { @Composable { Icon(imageVector = it, contentDescription = null) } } - ElementTheme(theme = Theme.Dark) { + ElementTheme(darkTheme = true) { IconsPreview( title = "Compound Vector Icons", content = content.toImmutableList() 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..2fe3167199 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 @@ -24,7 +24,6 @@ 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 org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config @@ -52,7 +51,7 @@ class MaterialYouThemeTest { } } captureRoboImage(file = screenshotFile("MaterialYou Theme - Dark.png")) { - ElementTheme(dynamicColor = true, theme = Theme.Dark) { + ElementTheme(dynamicColor = true, darkTheme = true) { Surface { Column( modifier = Modifier.padding(16.dp), diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt index 3f43cfea67..8fd7c5f041 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt @@ -15,7 +15,6 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test @@ -73,14 +72,4 @@ class ThemeTest { assertThat(awaitItem()).isTrue() } } - - @Test - fun `mapToTheme falls back to dark when black theme is disabled`() = runTest { - flowOf(Theme.Black.name) - .mapToTheme(allowBlackTheme = false) - .test { - assertThat(awaitItem()).isEqualTo(Theme.Dark) - awaitComplete() - } - } } diff --git a/libraries/cryptography/api/build.gradle.kts b/libraries/cryptography/api/build.gradle.kts index 74fc5f6ecc..9ce26419d8 100644 --- a/libraries/cryptography/api/build.gradle.kts +++ b/libraries/cryptography/api/build.gradle.kts @@ -13,7 +13,3 @@ 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 b210664d9f..ba6c10dbe0 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,7 +8,6 @@ package io.element.android.libraries.cryptography.api -import kotlinx.coroutines.flow.Flow import javax.crypto.SecretKey /** @@ -16,18 +15,16 @@ 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 */ - suspend fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey + fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey /** * Delete the secret key for a given alias. * @param alias the alias to use */ - suspend fun deleteKey(alias: String) + fun deleteKey(alias: String) } diff --git a/libraries/cryptography/impl/build.gradle.kts b/libraries/cryptography/impl/build.gradle.kts index 3a1f55126e..454432de6f 100644 --- a/libraries/cryptography/impl/build.gradle.kts +++ b/libraries/cryptography/impl/build.gradle.kts @@ -21,7 +21,6 @@ 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 bcd38695c4..46572ef047 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,16 +13,11 @@ 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 @@ -30,23 +25,13 @@ 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 suspend fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey { + override fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey { val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) ?.secretKey return if (secretKeyEntry == null) { @@ -61,22 +46,15 @@ class KeyStoreSecretKeyRepository( .setUserAuthenticationRequired(requiresUserAuthentication) .build() generator.init(keyGenSpec) - generator.generateKey().also { - hasKeyMap.getOrPut(alias) { - MutableStateFlow(true) - }.emit(true) - } + generator.generateKey() } else { secretKeyEntry } } - override suspend fun deleteKey(alias: String) { + override 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 5cf04a9754..eaa621d53a 100644 --- a/libraries/cryptography/test/build.gradle.kts +++ b/libraries/cryptography/test/build.gradle.kts @@ -16,5 +16,4 @@ 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 507325f45b..0e301553ea 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,39 +10,20 @@ 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() - 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 { + override fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey { return secretKeyForAlias.getOrPut(alias) { - generateKey().also { - hasKeyMap.getOrPut(alias) { - MutableStateFlow(true) - }.emit(true) - } + generateKey() } } - override suspend fun deleteKey(alias: String) { + override 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/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt index 07e55410d1..3cd004f8a8 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt @@ -27,9 +27,4 @@ class PreviewStringProvider( override fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String { return resources.getQuantityString(resId, quantity, *formatArgs) } - - override fun getSimpleQuantityString(resIdForOne: Int, resIdForOthers: Int, quantity: Int, vararg formatArgs: Any?): String { - val resId = if (quantity == 1) resIdForOne else resIdForOthers - return resources.getString(resId, *formatArgs) - } } diff --git a/libraries/dateformatter/impl/src/main/res/values-ca/translations.xml b/libraries/dateformatter/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 491793c728..0000000000 --- a/libraries/dateformatter/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "%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 d1ee550b73..9fab311a1d 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/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index c159659fb0..bdb9a32e89 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -38,7 +38,6 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.core) - implementation(projects.libraries.featureflag.api) implementation(projects.libraries.preferences.api) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) 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 a29e1b743c..b62f634ece 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, start = 8.dp, end = 8.dp), + modifier = Modifier.padding(bottom = 16.dp), title = title, subTitle = subTitle, iconStyle = iconStyle, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt index 0ff1533ea0..c2ee9800db 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/SunsetPage.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.element.android.compound.annotations.CoreColorToken import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.theme.Theme import io.element.android.compound.tokens.generated.internal.DarkColorTokens import io.element.android.compound.tokens.generated.internal.LightColorTokens import io.element.android.libraries.designsystem.R @@ -51,7 +50,7 @@ fun SunsetPage( ) { ElementTheme( // Always use the opposite value of the current theme - theme = if (ElementTheme.isLightTheme) Theme.Dark else Theme.Light, + darkTheme = ElementTheme.isLightTheme, applySystemBarsUpdate = false, ) { Box( 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 037c37e3a8..dcd3f8fa21 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,8 +24,6 @@ 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 @@ -150,11 +148,7 @@ private fun TitleAndDescription( text = title, style = ElementTheme.typography.fontBodyLgMedium, color = titleColor, - modifier = Modifier - .weight(1f) - .semantics { - heading() - }, + modifier = Modifier.weight(1f), ) 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 4940f2f965..9e783d605c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LocationPin.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LocationPin.kt @@ -54,7 +54,6 @@ 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 @@ -396,7 +395,7 @@ private object LocationPinRenderer { internal fun LocationPinPreview() = ElementPreview { val sampleAvatarData = AvatarData( id = "@alice:matrix.org", - name = USER_NAME_ALICE, + name = "Alice", url = null, size = AvatarSize.SelectedUser ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt index 9c3930ac64..34d119508a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/SimpleModalBottomSheet.kt @@ -14,8 +14,6 @@ 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.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -40,13 +38,11 @@ fun SimpleModalBottomSheet( onDismissRequest = onDismiss, modifier = modifier, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), - scrollable = false, ) { Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp) - .verticalScroll(rememberScrollState()), + .padding(16.dp), ) { Text( title, 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 53ef5593ed..870ffe44a9 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,12 +8,10 @@ package io.element.android.libraries.designsystem.components.avatar -import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE - fun anAvatarData( - // Let the id not start with a 'a'. + // Let's the id not start with a 'a'. id: String = "@id_of_alice:server.org", - name: String? = USER_NAME_ALICE, + name: String? = "Alice", url: String? = null, size: AvatarSize = AvatarSize.RoomListItem, ) = AvatarData( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 320457cedd..b1e3356fc3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -24,7 +24,7 @@ enum class AvatarSize(val dp: Dp) { RoomSelectRoomListItem(36.dp), - UserPreference(52.dp), + UserPreference(56.dp), UserHeader(96.dp), UserListItem(36.dp), @@ -65,7 +65,7 @@ enum class AvatarSize(val dp: Dp) { KnockRequestItem(52.dp), KnockRequestBanner(32.dp), - MediaSender(52.dp), + MediaSender(32.dp), DmCreationConfirmation(64.dp), 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 new file mode 100644 index 0000000000..c5b870507b --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/DmAvatars.kt @@ -0,0 +1,140 @@ +/* + * 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/preview/ElementPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt index e4030a77ea..b0ef41e154 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt @@ -19,7 +19,6 @@ import coil3.asImage import coil3.compose.AsyncImagePreviewHandler import coil3.compose.LocalAsyncImagePreviewHandler import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.theme.Theme import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.utils.CommonDrawables @@ -27,7 +26,7 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables @Composable @Suppress("ModifierMissing") fun ElementPreview( - theme: Theme = if (isSystemInDarkTheme()) Theme.Dark else Theme.Light, + darkTheme: Boolean = isSystemInDarkTheme(), showBackground: Boolean = true, @DrawableRes drawableFallbackForImages: Int = CommonDrawables.sample_background, @@ -39,7 +38,7 @@ fun ElementPreview( ResourcesCompat.getDrawable(context.resources, drawableFallbackForImages, null)!!.asImage() } ) { - ElementTheme(theme = theme) { + ElementTheme(darkTheme = darkTheme) { if (showBackground) { // If we have a proper contentColor applied we need a Surface instead of a Box Surface(content = content) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewBlack.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewBlack.kt deleted file mode 100644 index c10b4272e2..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewBlack.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.designsystem.preview - -import androidx.compose.runtime.Composable -import io.element.android.compound.theme.Theme - -@Composable -fun ElementPreviewBlack( - showBackground: Boolean = true, - content: @Composable () -> Unit -) { - ElementPreview( - theme = Theme.Black, - showBackground = showBackground, - content = content - ) -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewDark.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewDark.kt index 4f6898ff2b..c054b318f3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewDark.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewDark.kt @@ -8,22 +8,16 @@ package io.element.android.libraries.designsystem.preview -import androidx.annotation.DrawableRes import androidx.compose.runtime.Composable -import io.element.android.compound.theme.Theme -import io.element.android.libraries.designsystem.utils.CommonDrawables @Composable fun ElementPreviewDark( showBackground: Boolean = true, - @DrawableRes - drawableFallbackForImages: Int = CommonDrawables.sample_background, - content: @Composable () -> Unit, + content: @Composable () -> Unit ) { ElementPreview( - theme = Theme.Dark, + darkTheme = true, showBackground = showBackground, - drawableFallbackForImages = drawableFallbackForImages, - content = content, + content = content ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewLight.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewLight.kt index b52af233d8..1c2bdf3cef 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewLight.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreviewLight.kt @@ -8,22 +8,16 @@ package io.element.android.libraries.designsystem.preview -import androidx.annotation.DrawableRes import androidx.compose.runtime.Composable -import io.element.android.compound.theme.Theme -import io.element.android.libraries.designsystem.utils.CommonDrawables @Composable fun ElementPreviewLight( showBackground: Boolean = true, - @DrawableRes - drawableFallbackForImages: Int = CommonDrawables.sample_background, - content: @Composable () -> Unit, + content: @Composable () -> Unit ) { ElementPreview( - theme = Theme.Light, + darkTheme = false, showBackground = showBackground, - drawableFallbackForImages = drawableFallbackForImages, - content = content, + content = content ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt index a0bd489f8c..7b29757843 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementThemedPreview.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.Theme import io.element.android.libraries.designsystem.utils.CommonDrawables @Composable @@ -41,14 +40,14 @@ fun ElementThemedPreview( if (vertical) { Column { ElementPreview( - theme = Theme.Light, + darkTheme = false, showBackground = showBackground, drawableFallbackForImages = drawableFallbackForImages, content = content, ) Spacer(modifier = Modifier.height(4.dp)) ElementPreview( - theme = Theme.Dark, + darkTheme = true, showBackground = showBackground, drawableFallbackForImages = drawableFallbackForImages, content = content @@ -57,14 +56,14 @@ fun ElementThemedPreview( } else { Row { ElementPreview( - theme = Theme.Light, + darkTheme = false, showBackground = showBackground, drawableFallbackForImages = drawableFallbackForImages, content = content, ) Spacer(modifier = Modifier.width(4.dp)) ElementPreview( - theme = Theme.Dark, + darkTheme = true, showBackground = showBackground, drawableFallbackForImages = drawableFallbackForImages, content = content 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 deleted file mode 100644 index 66a6043634..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewData.kt +++ /dev/null @@ -1,27 +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.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/theme/ElementThemeApp.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt index 0c2fdff0ab..7aa0ab79b9 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt @@ -18,12 +18,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.Theme +import io.element.android.compound.theme.isDark import io.element.android.compound.theme.mapToTheme import io.element.android.compound.tokens.generated.SemanticColors 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.preferences.api.store.AppPreferencesStore val LocalBuildMeta = staticCompositionLocalOf { @@ -55,24 +54,21 @@ val LocalBuildMeta = staticCompositionLocalOf { @Composable fun ElementThemeApp( appPreferencesStore: AppPreferencesStore, - featureFlagService: FeatureFlagService, compoundLight: SemanticColors, compoundDark: SemanticColors, buildMeta: BuildMeta, content: @Composable () -> Unit, ) { - val isBlackThemeAllowed by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.AllowBlackTheme) - }.collectAsState(initial = false) - val theme by remember(isBlackThemeAllowed) { - appPreferencesStore.getThemeFlow().mapToTheme(allowBlackTheme = isBlackThemeAllowed) - }.collectAsState(initial = Theme.System) + val theme by remember { + appPreferencesStore.getThemeFlow().mapToTheme() + } + .collectAsState(initial = Theme.System) LaunchedEffect(theme) { AppCompatDelegate.setDefaultNightMode( when (theme) { Theme.System -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM Theme.Light -> AppCompatDelegate.MODE_NIGHT_NO - Theme.Dark, Theme.Black -> AppCompatDelegate.MODE_NIGHT_YES + Theme.Dark -> AppCompatDelegate.MODE_NIGHT_YES } ) } @@ -80,7 +76,7 @@ fun ElementThemeApp( LocalBuildMeta provides buildMeta, ) { ElementTheme( - theme = theme, + darkTheme = theme.isDark(), content = content, compoundLight = compoundLight, compoundDark = compoundDark, 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 3a86d72e7d..403ed6da97 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,8 +17,6 @@ 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 @@ -50,9 +48,6 @@ 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..2c577ec6b7 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 @@ -10,13 +10,10 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -45,15 +42,10 @@ import io.element.android.libraries.designsystem.preview.sheetStateForPreview import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -/** - * For parameter [scrollable], set it to true if the content of the sheet does not already contain a scrollable component, such as a LazyColumn, - * to avoid nested scroll issues. In this case, the content will be wrapped in a Column with verticalScroll. - */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun ModalBottomSheet( onDismissRequest: () -> Unit, - scrollable: Boolean, modifier: Modifier = Modifier, sheetState: SheetState = rememberModalBottomSheetState(), shape: Shape = BottomSheetDefaults.ExpandedShape, @@ -87,17 +79,8 @@ fun ModalBottomSheet( scrimColor = scrimColor, dragHandle = dragHandle, contentWindowInsets = contentWindowInsets, - ) { - if (scrollable) { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - ) { - content() - } - } else { - content() - } - } + content = content, + ) } @OptIn(ExperimentalMaterial3Api::class) @@ -108,11 +91,13 @@ fun SheetState.hide(coroutineScope: CoroutineScope, then: suspend () -> Unit) { } } +// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380 @Preview(group = PreviewGroup.BottomSheets) @Composable internal fun ModalBottomSheetLightPreview() = ElementPreviewLight { ContentToPreview() } +// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380 @Preview(group = PreviewGroup.BottomSheets) @Composable internal fun ModalBottomSheetDarkPreview() = @@ -127,7 +112,6 @@ private fun ContentToPreview() { ) { ModalBottomSheet( onDismissRequest = {}, - scrollable = false, ) { Text( text = "Sheet Content", 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 c8afba3603..7878245aca 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,28 +77,26 @@ class DefaultPinnedMessagesBannerFormatter( messageType.toPlainText(permalinkParser) } is VideoMessageType -> { - messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_video) + messageType.bestDescription.prefixWith(CommonStrings.common_video) } is ImageMessageType -> { - messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_image) + messageType.bestDescription.prefixWith(CommonStrings.common_image) } is StickerMessageType -> { - messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_sticker) + messageType.bestDescription.prefixWith(CommonStrings.common_sticker) } is LocationMessageType -> { messageType.body.prefixWith(CommonStrings.common_shared_location) } is FileMessageType -> { - messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_file) + messageType.bestDescription.prefixWith(CommonStrings.common_file) } is AudioMessageType -> { - messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_audio) + messageType.bestDescription.prefixWith(CommonStrings.common_audio) } is VoiceMessageType -> { - messageType - .toPlainText(permalinkParser, "") - .takeIf { it.isNotEmpty() } - ?.prefixWith(sp.getString(CommonStrings.common_voice_message)) + // 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)) ?: 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 e8a462da16..ae21f34dcb 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 @@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.roomlist.LatestEventValue import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent @@ -54,7 +55,6 @@ 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( @@ -93,8 +93,8 @@ class DefaultRoomLatestEventFormatter( message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) } is StickerContent -> { - content.bestDescription.prefixWith(sp.getString(CommonStrings.common_sticker)) - .prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) + val message = sp.getString(CommonStrings.common_sticker) + " (" + content.bestDescription + ")" + message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) } is UnableToDecryptContent -> { val message = sp.getString(CommonStrings.common_waiting_for_decryption_key) @@ -110,19 +110,20 @@ class DefaultRoomLatestEventFormatter( stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.RoomList) } is PollContent -> { - content.question.prefixWith(sp.getString(CommonStrings.common_poll_summary_prefix)) - .prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) + val message = sp.getString(CommonStrings.common_poll_summary, content.question) + message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) } is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> { val message = sp.getString(CommonStrings.common_unsupported_event) message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) } is LiveLocationContent -> { - val message = sp.getString(CommonStrings.common_shared_live_location) + val message = sp.getString(CommonStrings.common_shared_location) message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) } is LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call) - is CallNotifyContent -> rtcNotificationContentFormatter.format(content, isDmRoom) + is CallNotifyContent -> sp.getString(CommonStrings.common_call_started) + is CustomEventContent -> null }?.take(DEFAULT_SAFE_LENGTH) } @@ -140,28 +141,26 @@ class DefaultRoomLatestEventFormatter( messageType.toPlainText(permalinkParser) } is VideoMessageType -> { - messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_video)) + messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_video)) } is ImageMessageType -> { - messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_image)) + messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_image)) } is StickerMessageType -> { - messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_sticker)) + messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_sticker)) } is LocationMessageType -> { sp.getString(CommonStrings.common_shared_location) } is FileMessageType -> { - messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_file)) + messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_file)) } is AudioMessageType -> { - messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_audio)) + messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_audio)) } is VoiceMessageType -> { - messageType - .toPlainText(permalinkParser, "") - .takeIf { it.isNotEmpty() } - ?.prefixWith(sp.getString(CommonStrings.common_voice_message)) + // 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)) ?: sp.getString(CommonStrings.common_voice_message) } is OtherMessageType -> { diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt index ff5cce7a59..c32f2164e1 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.eventformatter.impl.mode.RenderingMode import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent @@ -71,7 +72,8 @@ class DefaultTimelineEventFormatter( is FailedToParseMessageLikeContent, is FailedToParseStateContent, is LiveLocationContent, - is UnknownContent -> { + is UnknownContent, + is CustomEventContent -> { if (buildMeta.isDebuggable) { error("You should not use this formatter for this event content: $content") } 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 1a04893e07..51fdccf256 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,10 +20,6 @@ internal fun CharSequence.prefixWith(prefix: String): AnnotatedString { append(prefix) } append(": ") - if (this@prefixWith is AnnotatedString) { - append(this@prefixWith) - } else { - append(this@prefixWith.toString()) - } + append(this@prefixWith) } } 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 deleted file mode 100644 index ab3fb9433d..0000000000 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatter.kt +++ /dev/null @@ -1,39 +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.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 deleted file mode 100644 index 2caa14e8a3..0000000000 --- a/libraries/eventformatter/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - "(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 94e1015375..e45227ad62 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ś do pokoju" + "Dołączyłeś(aś) 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-zh/translations.xml b/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml index f7e5d7d7f2..cd22ebed38 100644 --- a/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml @@ -1,6 +1,6 @@ - "(头像也已更换)" + "(头像也更改了)" "%1$s 更换了头像" "你更换了头像" "%1$s 降级为成员" @@ -13,61 +13,61 @@ "你将显示名称设置为 %1$s" "%1$s 晋升为管理员" "%1$s 晋升为协管员" - "%1$s 更换了房间头像" - "你更换了房间头像" - "%1$s 移除了房间头像" - "你移除了房间头像" - "%1$s 已封禁 %2$s" - "你已封禁 %1$s" - "你已封禁 %1$s:%2$s" - "%1$s 已封禁 %2$s:%3$s" - "%1$s 创建了房间" - "你创建了房间" + "%1$s 更换了聊天室头像" + "你更换了聊天室头像" + "%1$s 移除了聊天室头像" + "你移除了聊天室头像" + "%1$s 封禁了 %2$s" + "你封禁了 %1$s" + "你封禁了%1$s:%2$s" + "%1$s封禁了%2$s:%3$s" + "%1$s 创建了聊天室" + "你创建了聊天室" "%1$s 邀请了 %2$s" "%1$s 接受了邀请" "你接受了邀请" "你邀请了 %1$s" "%1$s 邀请了你" - "%1$s 加入了房间" - "你加入了房间" - "%1$s 申请加入" - "%1$s 已允许 %2$s 加入" - "你已允许 %1$s 加入" - "你已申请加入" - "%1$s 拒绝了 %2$s 的加入申请" - "你拒绝了 %1$s 的加入申请" - "%1$s 拒绝了你的加入申请" + "%1$s 加入了聊天室" + "你加入了聊天室" + "%1$s 请求加入" + "%1$s 允许 %2$s 加入" + "您已允许 %1$s 加入" + "你已请求加入" + "%1$s 拒绝了 %2$s 的加入请求" + "你拒绝了 %1$s 的加入请求" + "%1$s 拒绝了你的加入请求" "%1$s 已不再想加入" "你取消了加入申请" - "%1$s 离开了房间" - "你离开了房间" - "%1$s 将房间名称更改为 %2$s" - "你将房间名称更改为 %1$s" - "%1$s 移除了房间名称" - "你移除了房间名称" - "%1$s 未产生任何更改" - "你未产生任何更改" - "%1$s 更改了已置顶的消息" - "你更改了已置顶的消息" - "%1$s 置顶了 1 个消息" - "你已置顶了 1 个消息" - "%1$s 取消置顶了 1 个消息" - "你取消置顶了 1 个消息" + "%1$s 离开了聊天室" + "你离开了聊天室" + "%1$s 将聊天室名称改为 %2$s" + "你把聊天室名称改为 %1$s" + "%1$s 移除了聊天室名称" + "你移除了聊天室名称" + "%1$s 没有任何更改" + "您未进行任何更改" + "%1$s 更改了置顶消息" + "您更改了置顶消息" + "%1$s 置顶了一条消息" + "您置顶了一条消息" + "%1$s 取消置顶了一条消息" + "您取消置顶了一条消息" "%1$s 拒绝了邀请" - "你拒绝了邀请" + "您拒绝了邀请" "%1$s 已移除 %2$s" - "你移除了 %1$s" - "你移除了 %1$s:%2$s" + "您移除了 %1$s" + "您移除了 %1$s:%2$s" "%1$s 移除了 %2$s:%3$s" - "%1$s 向 %2$s 发送了加入房间的邀请" - "你邀请 %1$s 加入房间" - "%1$s 撤消了 %2$s 加入房间的邀请" - "你撤消了 %1$s 加入房间的邀请" - "%1$s 将主题更改为:%2$s" - "你将主题更改为:%1$s" - "%1$s 移除了房间主题" - "你移除了房间主题" - "%1$s 已解封 %2$s" - "你已解封 %1$s" + "%1$s 向 %2$s 发送了加入聊天室的邀请" + "你邀请 %1$s 加入聊天室" + "%1$s 撤销了 %2$s 加入聊天室的邀请" + "你撤销了 %1$s 加入聊天室的邀请" + "%1$s 将主题改为:%2$s" + "你将主题改为:%1$s" + "%1$s 移除了聊天室主题" + "你移除了聊天室主题" + "%1$s 解禁了 %2$s" + "你解禁了 %1$s" "%1$s 对其成员资格进行了未知更改" diff --git a/libraries/eventformatter/impl/src/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 2e55fb0999..e1e8717c4c 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt @@ -10,7 +10,6 @@ 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 @@ -75,8 +74,7 @@ class DefaultRoomLatestEventFormatterTest { roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider), profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), stateContentFormatter = StateContentFormatter(stringProvider), - rtcNotificationContentFormatter = RtcNotificationContentFormatter(fakeMatrixClient, stringProvider), - permalinkParser = FakePermalinkParser() + permalinkParser = FakePermalinkParser(), ) } @@ -104,14 +102,7 @@ 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" - // 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) + val expectedBody = someoneElseId.value + ": Sticker (a sticker body)" assertThat(result.toString()).isEqualTo(expectedBody) } @@ -917,18 +908,10 @@ class DefaultRoomLatestEventFormatterTest { val pollContent = aPollContent() val mineContentEvent = createLatestEvent(sentByYou = true, senderDisplayName = "Alice", content = pollContent) - assertThat(formatter.format(mineContentEvent, true).toString()).isEqualTo("Poll: Do you like polls?") + assertThat(formatter.format(mineContentEvent, true)).isEqualTo("Poll: Do you like polls?") val contentEvent = createLatestEvent(sentByYou = false, senderDisplayName = "Bob", content = pollContent) - 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) + assertThat(formatter.format(contentEvent, true)).isEqualTo("Poll: Do you like polls?") } @Test @@ -941,15 +924,6 @@ 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 deleted file mode 100644 index dca34db3cc..0000000000 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatterTest.kt +++ /dev/null @@ -1,99 +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.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 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 -import kotlin.toString - -@Suppress("LargeClass") -@RunWith(RobolectricTestRunner::class) -class RtcNotificationContentFormatterTest { - 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 097df99800..eaa32e8adc 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -22,6 +22,13 @@ 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", @@ -45,6 +52,17 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + EnableKeyShareOnInvite( + key = "feature.enableKeyShareOnInvite", + title = "Share encrypted history with new members", + description = "When inviting a user to an encrypted room that has history visibility set to \"shared\"," + + " share encrypted history with that user, and accept encrypted history when you are invited to such a room." + + "\nRequires an app restart to take effect." + + "\n\nWARNING: this feature is EXPERIMENTAL and not all security precautions are implemented." + + " Do not enable on production accounts.", + defaultValue = { false }, + isFinished = false, + ), Knock( key = "feature.knock", title = "Ask to join", @@ -86,6 +104,14 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + SyncNotificationsWithWorkManager( + key = "feature.sync_notifications_with_workmanager", + title = "Sync notifications with WorkManager", + description = "Use WorkManager to schedule notification sync tasks when a push is received." + + " This should improve reliability and battery usage.", + defaultValue = { true }, + isFinished = false, + ), QrCodeLogin( key = "feature.qr_code_login", title = "QR Code Login", @@ -93,10 +119,17 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), - AllowBlackTheme( - key = "feature.allow_black_theme", - title = "Black theme", - description = "Allow selecting the black appearance theme for battery saving on OLED.", + 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, + ), + LiveLocationSharing( + key = "feature.liveLocationSharing", + title = "Live location sharing", + description = "Allow sharing live location in rooms.", defaultValue = { false }, isFinished = false, ), @@ -129,12 +162,4 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), - AutomaticBackPagination( - key = "feature.automatic_back_pagination", - title = "Automatic back pagination of rooms", - description = "Allow the app to automatically back paginate in rooms to pre-fetch older messages in background." + - "\nRequires an app restart to take effect.", - defaultValue = { false }, - isFinished = false, - ), } diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index 2a326527df..1c70006cc8 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -44,12 +44,14 @@ android { } dependencies { - implementation(libs.coroutines.core) - implementation(libs.serialization.json) + implementation(projects.libraries.di) implementation(projects.libraries.androidutils) - implementation(projects.libraries.architecture) - implementation(projects.libraries.sessionStorage.api) + 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) 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 bd7b1399e1..cd471e7df4 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 @@ -20,13 +20,14 @@ 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.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.walletsecretstorage.WalletSecretStorage 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.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.oauth.AccountManagementAction +import io.element.android.libraries.matrix.api.oidc.AccountManagementAction 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,7 +35,6 @@ 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 @@ -62,13 +62,13 @@ interface MatrixClient { val notificationService: NotificationService val notificationSettingsService: NotificationSettingsService val encryptionService: EncryptionService + val walletSecretStorage: WalletSecretStorage val roomDirectoryService: RoomDirectoryService val mediaPreviewService: MediaPreviewService val matrixMediaLoader: MatrixMediaLoader 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 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 f515ce8c32..ac3b0c8e3a 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().isDm, + isDM = info().isDirect, 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 d5fd6a734a..c50ec09609 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 OAuth(message: String?) : AuthenticationException(message) + class Oidc(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 04d1d13593..7c82668242 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt @@ -37,21 +37,21 @@ interface MatrixAuthenticationService { suspend fun importCreatedSession(externalSession: ExternalSession): Result /* - * OAuth part. + * OIDC part. */ /** - * Get the OAuth url to display to the user. + * Get the Oidc url to display to the user. */ - suspend fun getOAuthUrl( - prompt: OAuthPrompt, + suspend fun getOidcUrl( + prompt: OidcPrompt, loginHint: String?, - ): Result + ): Result /** - * Cancel OAuth login sequence. + * Cancel Oidc login sequence. */ - suspend fun cancelOAuthLogin(): Result + suspend fun cancelOidcLogin(): Result /** * Set the existing data about Element Classic session, if any. @@ -68,9 +68,9 @@ interface MatrixAuthenticationService { ): Boolean /** - * Attempt to log in using the [callbackUrl] provided by the OAuth page. + * Attempt to login using the [callbackUrl] provided by the Oidc page. */ - suspend fun loginWithOAuth(callbackUrl: String): Result + suspend fun loginWithOidc(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 8dcb5c4a48..aa5ed9a41d 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 supportsOAuthLogin: Boolean, + val supportsOidcLogin: Boolean, ) { - val isSupported = supportsPasswordLogin || supportsOAuthLogin + val isSupported = supportsPasswordLogin || supportsOidcLogin } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthConfig.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt similarity index 97% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthConfig.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt index d3a42f42b9..ee8b7ec50e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthConfig.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.api.auth import io.element.android.libraries.matrix.api.BuildConfig -object OAuthConfig { +object OidcConfig { 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/OAuthDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt similarity index 94% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthDetails.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt index d504f891ee..c4fb87e3c2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt @@ -12,6 +12,6 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class OAuthDetails( +data class OidcDetails( val url: String, ) : Parcelable diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthPrompt.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt similarity index 81% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthPrompt.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt index 45b4e18533..8ddad9f52e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthPrompt.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt @@ -8,12 +8,12 @@ package io.element.android.libraries.matrix.api.auth -sealed interface OAuthPrompt { +sealed interface OidcPrompt { /** * The Authorization Server should prompt the End-User for * reauthentication. */ - data object Login : OAuthPrompt + data object Login : OidcPrompt /** * The Authorization Server should prompt the End-User to create a user @@ -21,10 +21,10 @@ sealed interface OAuthPrompt { * * Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html). */ - data object Create : OAuthPrompt + data object Create : OidcPrompt /** * An unknown value. */ - data class Unknown(val value: String) : OAuthPrompt + data class Unknown(val value: String) : OidcPrompt } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthRedirectUrlProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcRedirectUrlProvider.kt similarity index 89% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthRedirectUrlProvider.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcRedirectUrlProvider.kt index 669d47501d..ad4d862474 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthRedirectUrlProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcRedirectUrlProvider.kt @@ -8,6 +8,6 @@ package io.element.android.libraries.matrix.api.auth -interface OAuthRedirectUrlProvider { +interface OidcRedirectUrlProvider { 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 3f22244405..a3b567fa46 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 OAuthMetadataInvalid : QrLoginException() + data object OidcMetadataInvalid : 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 333cfb709b..aefad517dc 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 @@ -112,19 +112,19 @@ interface IdentityPasswordResetHandle : IdentityResetHandle { } /** - * A handle to reset the user's identity with an OAuth login type. + * A handle to reset the user's identity with an OIDC login type. */ -interface IdentityOAuthResetHandle : IdentityResetHandle { +interface IdentityOidcResetHandle : IdentityResetHandle { /** * The URL to open in a webview/custom tab to reset the identity. */ val url: String /** - * Reset the identity using the OAuth flow. + * Reset the identity using the OIDC 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 resetOAuth(): Result + suspend fun resetOidc(): Result } 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 1947729c7f..0c261cdd1a 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 @@ -18,7 +18,6 @@ sealed interface LinkMobileStep { data object Uninitialized : 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 7ae1ca4886..4d8ce8afb4 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, isDM: Boolean): Result + suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: 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/oauth/AccountManagementAction.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oidc/AccountManagementAction.kt similarity index 91% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oauth/AccountManagementAction.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oidc/AccountManagementAction.kt index 6dd0f6e53c..e1c7764e58 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oauth/AccountManagementAction.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oidc/AccountManagementAction.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.matrix.api.oauth +package io.element.android.libraries.matrix.api.oidc import io.element.android.libraries.matrix.api.core.DeviceId 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 f7f4924d5a..589f88e9fd 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,14 @@ interface BaseRoom : Closeable { */ fun info(): RoomInfo = roomInfoFlow.value - /** - * Returns whether the [BaseRoom] is a DM, with an updated state from the latest [RoomInfo]. - */ - fun isDm() = roomInfoFlow.value.isDm - 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). + */ + val isOneToOne: Boolean get() = info().activeMembersCount == 2L + /** * Try to load the room members and update the membersFlow. */ 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 f3fefab9a8..6a307f6e62 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 containing the [EventId] of the beacon state event on success or an error on failure. + * @return Result indicating success or failure. */ - suspend fun startLiveLocationShare(durationMillis: Long): Result + suspend fun startLiveLocationShare(durationMillis: Long): Result /** * Stop sharing live location in this room. @@ -212,4 +212,16 @@ interface JoinedRoom : BaseRoom { * @return Result indicating success or failure. */ suspend fun sendLiveLocation(geoUri: String): Result + + /** + * Send a custom/raw event to the room (non-message event types). + * + * Used by the Cardano wallet for `/pay` events + * (e.g., `co.sulkta.payment.request`). Upstream SDK moved raw event + * sending from Timeline to Room; this method proxies through. + * + * @param eventType The custom event type string + * @param content The JSON-serialized event content + */ + suspend fun sendRawEvent(eventType: String, content: 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 e590976cab..5247e402a6 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,7 +29,6 @@ data class RoomInfo( val avatarUrl: String?, val isPublic: Boolean?, val isDirect: Boolean, - val isDm: Boolean, val isEncrypted: Boolean?, val joinRule: JoinRule?, val isSpace: Boolean, @@ -79,7 +78,6 @@ 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 new file mode 100644 index 0000000000..f33319e2ee --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheck.kt @@ -0,0 +1,32 @@ +/* + * 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 abf685a38b..0b3d7071c8 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,7 +22,6 @@ 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 2c93fa94f0..1c35fab7d0 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.isServiceMember && it.userId != sessionId && it.membership.isActive() } + ?.find { 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 deleted file mode 100644 index 358dcd98d1..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/BeaconId.kt +++ /dev/null @@ -1,12 +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.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 deleted file mode 100644 index 0b7e9b0f44..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/BeaconInfoUpdate.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.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 deleted file mode 100644 index 9b53603042..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/LiveLocationException.kt +++ /dev/null @@ -1,14 +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.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 4d8bc4638a..7e841639bd 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 @@ -15,21 +15,10 @@ import io.element.android.libraries.matrix.api.core.UserId data class LiveLocationShare( /** The user who is sharing their location. */ val userId: UserId, - /** The last known location if any. */ - val lastLocation: LastLocation?, - /** The timestamp when location sharing started, in milliseconds.*/ - 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( /** The last known geo URI (e.g., "geo:51.5074,-0.1278"). */ - val geoUri: String, + val lastGeoUri: String, /** The timestamp of the last location update. */ - val timestamp: Long, - /** The asset of the last location update. */ - val assetType: AssetType, + val lastTimestamp: Long, + /** Whether the live location share is still active. */ + val isLive: Boolean, ) 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 6606465389..e8f88ed86d 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,6 +19,4 @@ 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 3441a4be3a..6db326c854 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,6 +12,7 @@ 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 10ca376105..aca093eab6 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 isDm = info.isDm + val isOneToOne get() = info.activeMembersCount == 2L } 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 e22b0a5c9c..6a72577760 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,7 +38,6 @@ 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/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index fe73230dce..670ed8bb9c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -73,6 +73,18 @@ interface Timeline : AutoCloseable { asPlainText: Boolean = false, ): Result + /** + * Send a raw/custom event to the room. + * + * @param eventType The event type (e.g., "co.sulkta.payment.request") + * @param content The JSON content of the event + * @return Result indicating success or failure + */ + suspend fun sendRaw( + eventType: String, + content: String, + ): Result + suspend fun editMessage( eventOrTransactionId: EventOrTransactionId, body: String, 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 f323b316f7..993a8b759f 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 @@ -12,7 +12,6 @@ import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.location.AssetType @@ -106,21 +105,27 @@ data class FailedToParseStateContent( ) : EventContent data class LiveLocationContent( + val body: String, val isLive: Boolean, val description: String?, - val startTimestamp: Long, val timeout: Long, val assetType: AssetType?, val locations: List, -) : EventContent { - val endTimestamp = startTimestamp + timeout -} +) : EventContent data object LegacyCallInviteContent : EventContent -data class CallNotifyContent( - val callIntent: CallIntent, - val declinedBy: List -) : EventContent +data object CallNotifyContent : EventContent data object UnknownContent : EventContent + +/** + * Content for custom/unknown message-like events that we want to handle specially. + * + * @param eventType The Matrix event type (e.g., "co.sulkta.payment.request") + * @param rawJson The raw JSON content of the event, if available + */ +data class CustomEventContent( + val eventType: String, + val rawJson: String?, +) : 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 38a00d833d..4d2e786038 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 requestDeviceVerification() + suspend fun requestCurrentSessionVerification() /** * Request verification of the user with the given [userId]. @@ -56,9 +56,9 @@ interface SessionVerificationService { suspend fun declineVerification() /** - * Transition the current verification request into a SAS verification flow. + * Starts the verification of the unverified session from another device. */ - suspend fun startSasVerification() + suspend fun startVerification() /** * Returns the verification service state to the initial step. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/walletsecretstorage/WalletSecretStorage.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/walletsecretstorage/WalletSecretStorage.kt new file mode 100644 index 0000000000..885f5f4b03 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/walletsecretstorage/WalletSecretStorage.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.libraries.matrix.api.walletsecretstorage + +/** + * Stores the Cardano wallet seed phrase encrypted in Matrix account data. + * + * Design summary: + * - Slot name: a namespace we own (not a Matrix-spec 4S slot) + * - Encryption: AES-256-GCM keyed by HKDF-SHA256 of the user's Matrix + * recovery-key entropy + a wallet-specific info label. The info label + * guarantees wallet-derived keys never collide with Matrix's own crypto keys. + * - Storage: Matrix account data via the Rust SDK (Client.setAccountData / + * Client.accountData). The homeserver never sees plaintext. + * - Recovery: the user's existing Matrix recovery key is the sole secret — + * same key they memorised when setting up Matrix crypto backup. + * + * See libraries/matrix/impl/.../walletsecretstorage/{WalletSecretEnvelope, + * RecoveryKeyDerivation}.kt for the envelope format + derivation details. + */ +interface WalletSecretStorage { + /** + * Encrypt and store [seedPhrase] under the user's recovery key. + * + * @param recoveryKey The user's Matrix recovery key (whitespace tolerated). + * @param seedPhrase The wallet seed to back up. Normally a space-separated + * BIP-39 mnemonic; any UTF-8 string is accepted. + * @return Success on write; [WalletSecretStorageException.InvalidRecoveryKey] + * if the recovery key is malformed. + */ + suspend fun putSeed(recoveryKey: String, seedPhrase: String): Result + + /** + * Fetch and decrypt the wallet seed. + * + * @param recoveryKey The user's Matrix recovery key. + * @return Success(null) if no backup exists or the envelope can't be decoded; + * Success(String) with the decrypted seed phrase if it unlocks; + * Failure([WalletSecretStorageException.InvalidRecoveryKey]) if the + * input isn't a valid recovery key format. + * + * Note: we deliberately do NOT distinguish "wrong recovery key" from + * "tampered blob" in the success path — both surface as null, mirroring + * GCM's authenticated-decryption contract. + */ + suspend fun getSeed(recoveryKey: String): Result + + /** + * Whether an encrypted wallet-seed blob currently exists in account data. + * Doesn't need the recovery key; useful for onboarding ("restore from backup?"). + */ + suspend fun hasSeedBackup(): Result + + /** + * Delete the stored blob from account data. Irreversible. + */ + suspend fun deleteSeed(): Result +} + +sealed class WalletSecretStorageException(message: String) : Exception(message) { + object InvalidRecoveryKey : WalletSecretStorageException( + "Recovery key is not a valid Matrix recovery key (wrong format, prefix, or parity)." + ) + object WriteFailed : WalletSecretStorageException("Failed to write wallet seed to account data.") + object ReadFailed : WalletSecretStorageException("Failed to read wallet seed from account data.") +} 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 9babb7a738..d4b360ef53 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( - supportsOAuthLogin = true, + supportsOidcLogin = 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( - supportsOAuthLogin = false, + supportsOidcLogin = 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( - supportsOAuthLogin = true, + supportsOidcLogin = 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( - supportsOAuthLogin = false, + supportsOidcLogin = 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 new file mode 100644 index 0000000000..1461d28fd9 --- /dev/null +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheckTest.kt @@ -0,0 +1,54 @@ +/* + * 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 0a95147f21..eae96b5cd9 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -28,17 +28,14 @@ dependencies { } else { debugImplementation(libs.matrix.sdk) } - implementation(projects.libraries.rustlsTls) + implementation(files("libs/rustls-platform-verifier-android.aar")) 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) @@ -50,7 +47,6 @@ dependencies { 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/libs/rustls-platform-verifier-android.aar b/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar new file mode 100644 index 0000000000..8acc8b5fe0 Binary files /dev/null and b/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar differ diff --git a/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar.version b/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar.version new file mode 100644 index 0000000000..a9cd6c6052 --- /dev/null +++ b/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar.version @@ -0,0 +1 @@ +Updated rustls-platform-verifier-android.aar using `rustls-platform-verifier-0.1.1.aar` 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 a5c69bf831..690995ba77 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.extensions.runCatchingExceptions +import io.element.android.libraries.core.coroutine.CoroutineDispatchers 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,7 +19,6 @@ 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 @@ -42,10 +41,14 @@ 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) @@ -63,24 +66,11 @@ 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) { - 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 - } - + appCoroutineScope.launch(updateTokensDispatcher) { + val existingData = sessionStore.getSession(session.userId) ?: return@launch val (anonymizedAccessToken, anonymizedRefreshToken) = session.anonymizedTokens() - Timber.tag(loggerTag.value).i( + Timber.tag(loggerTag.value).d( "Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " + "Was token valid: ${existingData.isTokenValid}" ) @@ -90,27 +80,28 @@ class RustClientSessionDelegate( passphrase = existingData.passphrase, sessionPaths = existingData.getSessionPaths(), ) - 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.") + 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.") + } } } - // 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) { - 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. + 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) { val currentClient = client.get() if (currentClient == null) { Timber.tag(loggerTag.value).w("didReceiveAuthError -> no client, exiting") isLoggingOut.set(false) - return + return@launch } - val existingData = runBlocking { sessionStore.getSession(currentClient.sessionId.value) } + val existingData = sessionStore.getSession(currentClient.sessionId.value) val (anonymizedAccessToken, anonymizedRefreshToken) = existingData.anonymizedTokens() Timber.tag(loggerTag.value).d( "Removing session data with access token '$anonymizedAccessToken' " + @@ -119,17 +110,19 @@ class RustClientSessionDelegate( if (existingData != null) { // Set isTokenValid to false val newData = existingData.copy(isTokenValid = false) - runBlocking { sessionStore.updateData(newData) } + 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.") } - appCoroutineScope.launch { currentClient.logout(userInitiated = false, ignoreSdkError = true) } - } else { - Timber.tag(loggerTag.value).v("didReceiveAuthError -> already cleaning up") + currentClient.logout(userInitiated = false, ignoreSdkError = true) + }.invokeOnCompletion { + if (it != null) { + Timber.tag(loggerTag.value).e(it, "Failed to remove session data.") + } } - }.onFailure { - Timber.tag(loggerTag.value).e(it, "Failed to remove session data.") + } else { + Timber.tag(loggerTag.value).v("didReceiveAuthError -> already cleaning up") } } 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 05e52605dc..48eee17eb2 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,7 @@ 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.oauth.AccountManagementAction +import io.element.android.libraries.matrix.api.oidc.AccountManagementAction 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 @@ -50,6 +50,7 @@ import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService +import io.element.android.libraries.matrix.impl.walletsecretstorage.MatrixAccountDataWalletSecretStorage import io.element.android.libraries.matrix.impl.exception.mapClientException import io.element.android.libraries.matrix.impl.linknewdevice.RustLinkDesktopHandler import io.element.android.libraries.matrix.impl.linknewdevice.RustLinkMobileHandler @@ -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.oauth.toRustAction +import io.element.android.libraries.matrix.impl.oidc.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,7 +71,6 @@ 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 @@ -114,8 +114,6 @@ 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 @@ -182,6 +180,8 @@ class RustMatrixClient( dispatchers = dispatchers, ) + override val walletSecretStorage = MatrixAccountDataWalletSecretStorage(innerClient, dispatchers) + override val roomDirectoryService = RustRoomDirectoryService( client = innerClient, sessionDispatcher = sessionDispatcher, @@ -210,15 +210,6 @@ 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 }, 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 c22a8b9454..f83efd2736 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 @@ -41,7 +41,6 @@ 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 @@ -72,6 +71,7 @@ class RustMatrixClientFactory( sessionStore = sessionStore, appCoroutineScope = appCoroutineScope, analyticsService = analyticsService, + coroutineDispatchers = coroutineDispatchers ) suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { @@ -105,11 +105,6 @@ class RustMatrixClientFactory( suspend fun create(client: Client): RustMatrixClient { val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens() - // Must be called before creating the sync service, timelines etc. - if (featureFlagService.isFeatureEnabled(FeatureFlags.AutomaticBackPagination)) { - client.enableAutomaticBackpagination() - } - client.setUtdDelegate(UtdTracker(analyticsService)) val syncService = client.syncService() @@ -131,7 +126,7 @@ class RustMatrixClientFactory( analyticsService = analyticsService, workManagerScheduler = workManagerScheduler, ).also { - Timber.tag("RustMatrixClient").i("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'") + Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'") } } @@ -167,9 +162,8 @@ class RustMatrixClientFactory( } ) ) - .enableShareHistoryOnInvite(true) + .enableShareHistoryOnInvite(featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite)) .threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.Threads), threadSubscriptions = false) - .dmRoomDefinition(DmRoomDefinition.TWO_MEMBERS) .requestConfig( RequestConfig( timeout = 30_000uL, @@ -215,5 +209,5 @@ fun SessionData.toSession() = Session( deviceId = deviceId, homeserverUrl = homeserverUrl, slidingSyncVersion = SlidingSyncVersion.NATIVE, - oauthData = oAuthData, + oidcData = oidcData, ) 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 8a0a246c22..263ce15bd3 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,6 +11,7 @@ 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/analytics/RustAnalyticsSdkManager.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/RustAnalyticsSdkManager.kt index a74acab683..be91571efd 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/RustAnalyticsSdkManager.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/RustAnalyticsSdkManager.kt @@ -11,12 +11,12 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.services.analytics.api.AnalyticsSdkManager import io.element.android.services.analytics.api.AnalyticsSdkSpan -import org.matrix.rustcomponents.sdk.enableSentryLogging @ContributesBinding(AppScope::class) class RustAnalyticsSdkManager : AnalyticsSdkManager { override fun enableSdkAnalytics(enabled: Boolean) { - enableSentryLogging(enabled) + // Sentry logging was removed from the Rust SDK + // This is now a no-op } override fun startSpan(name: String, parentTraceId: String?): AnalyticsSdkSpan { 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 fac5227f6a..ebe0c5e4e8 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.OAuthException +import org.matrix.rustcomponents.sdk.OidcException fun Throwable.mapAuthenticationException(): AuthenticationException { return when (this) { @@ -28,14 +28,13 @@ fun Throwable.mapAuthenticationException(): AuthenticationException { } is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message) is ClientBuildException.EventCache -> AuthenticationException.Generic(message) - is ClientBuildException.InvalidRawKey -> AuthenticationException.Generic(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) + 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) } 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 810b8d8e0d..acf96d69d4 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(), - supportsOAuthLogin = supportsOauthLogin(), + supportsOidcLogin = supportsOidcLogin(), ) } 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 deleted file mode 100644 index d80dea2e7f..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OAuthConfigurationProvider.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.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 new file mode 100644 index 0000000000..6f9dd67b12 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProvider.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.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 e97ddbee84..e21d8d94c6 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.OAuthPrompt -import org.matrix.rustcomponents.sdk.OAuthPrompt as RustOAuthPrompt +import io.element.android.libraries.matrix.api.auth.OidcPrompt +import org.matrix.rustcomponents.sdk.OidcPrompt as RustOidcPrompt -internal fun OAuthPrompt.toRustPrompt(): RustOAuthPrompt { +internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt { return when (this) { - OAuthPrompt.Login -> RustOAuthPrompt.Unknown("consent") - OAuthPrompt.Create -> RustOAuthPrompt.Create - is OAuthPrompt.Unknown -> RustOAuthPrompt.Unknown(value) + OidcPrompt.Login -> RustOidcPrompt.Unknown("consent") + OidcPrompt.Create -> RustOidcPrompt.Create + is OidcPrompt.Unknown -> RustOidcPrompt.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 3f8893138f..0603fddec4 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 @@ -31,8 +31,8 @@ class RustHomeServerLoginCompatibilityChecker( it.homeserverLoginDetails() } .use { - Timber.d("Homeserver $url | OAuth: ${it.supportsOauthLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}") - it.supportsOauthLogin() || it.supportsPasswordLogin() + Timber.d("Homeserver $url | OIDC: ${it.supportsOidcLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}") + it.supportsOidcLogin() || 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 692edf79ae..9fe7c7cd1f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -11,7 +11,6 @@ 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.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.runCatchingExceptions @@ -20,8 +19,8 @@ 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.OAuthDetails -import io.element.android.libraries.matrix.api.auth.OAuthPrompt +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.SessionRestorationException import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData @@ -66,8 +65,7 @@ class RustMatrixAuthenticationService( private val sessionStore: SessionStore, private val rustMatrixClientFactory: RustMatrixClientFactory, private val passphraseGenerator: PassphraseGenerator, - private val oAuthConfigurationProvider: OAuthConfigurationProvider, - private val enterpriseService: EnterpriseService, + private val oidcConfigurationProvider: OidcConfigurationProvider, ) : MatrixAuthenticationService { // Any existing Element Classic session that we want to try to import secrets from during login. private var elementClassicSession: ElementClassicSession? = null @@ -255,15 +253,15 @@ class RustMatrixAuthenticationService( private var pendingOAuthAuthorizationData: OAuthAuthorizationData? = null - override suspend fun getOAuthUrl( - prompt: OAuthPrompt, + override suspend fun getOidcUrl( + prompt: OidcPrompt, loginHint: String?, - ): Result { + ): Result { return withContext(coroutineDispatchers.io) { runCatchingExceptions { val client = currentClient ?: error("You need to call `setHomeserver()` first") - val oAuthAuthorizationData = client.urlForOauth( - oauthConfiguration = oAuthConfigurationProvider.get(), + val oAuthAuthorizationData = client.urlForOidc( + oidcConfiguration = oidcConfigurationProvider.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 @@ -271,30 +269,24 @@ class RustMatrixAuthenticationService( additionalScopes = emptyList(), ) val url = oAuthAuthorizationData.loginUrl() - .let { - enterpriseService.tweakMasUrl( - url = it, - homeserver = client.server() ?: client.homeserver(), - ) - } pendingOAuthAuthorizationData = oAuthAuthorizationData - OAuthDetails(url) + OidcDetails(url) }.mapFailure { failure -> - Timber.e(failure, "Failed to get OAuth URL") + Timber.e(failure, "Failed to get OIDC URL") failure.mapAuthenticationException() } } } - override suspend fun cancelOAuthLogin(): Result { + override suspend fun cancelOidcLogin(): Result { return withContext(coroutineDispatchers.io) { runCatchingExceptions { pendingOAuthAuthorizationData?.use { - currentClient?.abortOauthAuth(it) + currentClient?.abortOidcAuth(it) } pendingOAuthAuthorizationData = null }.mapFailure { failure -> - Timber.e(failure, "Failed to cancel OAuth login") + Timber.e(failure, "Failed to cancel OIDC login") failure.mapAuthenticationException() } } @@ -305,14 +297,14 @@ class RustMatrixAuthenticationService( } /** - * callbackUrl should be the `url` from `OAuthAction` (with all the parameters). + * callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters). */ - override suspend fun loginWithOAuth(callbackUrl: String): Result { + override suspend fun loginWithOidc(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.loginWithOauthCallback( + client.loginWithOidcCallback( callbackUrl = callbackUrl, ) // Free the pending data since we won't use it to abort the flow anymore @@ -338,7 +330,7 @@ class RustMatrixAuthenticationService( SessionId(sessionData.userId) }.mapFailure { failure -> - Timber.e(failure, "Failed to login with OAuth") + Timber.e(failure, "Failed to login with OIDC") failure.mapAuthenticationException() } } @@ -363,7 +355,7 @@ class RustMatrixAuthenticationService( withContext(coroutineDispatchers.io) { val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData val emptySessionPaths = rotateSessionPath() - val oAuthConfiguration = oAuthConfigurationProvider.get() + val oidcConfiguration = oidcConfigurationProvider.get() val progressListener = object : QrLoginProgressListener { override fun onUpdate(state: QrLoginProgress) { Timber.d("QR Code login progress: $state") @@ -376,7 +368,7 @@ class RustMatrixAuthenticationService( qrCodeData = sdkQrCodeLoginData, ) client.newLoginWithQrCodeHandler( - oauthConfiguration = oAuthConfiguration, + oidcConfiguration = oidcConfiguration, ).use { it.scan( qrCodeData = qrCodeData.rustQrCodeData, @@ -434,30 +426,13 @@ 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, slidingSyncType = ClientBuilderSlidingSync.Discovered, ) - .serverNameOrHomeserverUrl(baseUrlOrServerName) + .serverNameOrHomeserverUrl(qrCodeData.serverName()!!) .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 23b9c6be5e..ae56cb10fa 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.OAuthMetadataInvalid -> QrLoginException.OAuthMetadataInvalid + is RustHumanQrLoginException.OidcMetadataInvalid -> QrLoginException.OidcMetadataInvalid 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/encryption/RustIdentityResetHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt index 7ee9c7d0b3..4813ec1cc3 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.IdentityOAuthResetHandle +import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle 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.OAuth -> RustIdentityOAuthResetHandle(identityResetHandle, authType.info.approvalUrl) + is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl) // User interactive authentication (user + password) CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle) } @@ -47,11 +47,11 @@ class RustPasswordIdentityResetHandle( } } -class RustIdentityOAuthResetHandle( +class RustOidcIdentityResetHandle( private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle, override val url: String, -) : IdentityOAuthResetHandle { - override suspend fun resetOAuth(): Result { +) : IdentityOidcResetHandle { + override suspend fun resetOidc(): Result { return runCatchingExceptions { identityResetHandle.reset(null) } } 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 83e657eb07..211bdc3d4e 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,8 +54,6 @@ 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 cb387a9d21..0189987d96 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,17 +49,6 @@ class RustLinkMobileHandler( } } ) - // We emit Done in case the progress listener was deallocated before generate() sent the Done - _linkMobileStep.emit(LinkMobileStep.Done) - } 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 dfda37d7e1..3199ebf71a 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 @@ -27,7 +27,7 @@ internal fun Session.toSessionData( accessToken = accessToken, refreshToken = refreshToken, homeserverUrl = homeserverUrl ?: this.homeserverUrl, - oAuthData = oauthData, + oidcData = oidcData, loginTimestamp = Date(), isTokenValid = isTokenValid, loginType = loginType, @@ -52,7 +52,7 @@ internal fun ExternalSession.toSessionData( accessToken = accessToken, refreshToken = refreshToken, homeserverUrl = homeserverUrl, - oAuthData = null, + oidcData = 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 bfb49d6ceb..f1996dd942 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,6 +17,7 @@ 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 @@ -36,6 +37,10 @@ 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, @@ -45,10 +50,10 @@ class NotificationMapper( senderAvatarUrl = item.senderInfo.avatarUrl, senderDisplayName = item.senderInfo.displayName, senderIsNameAmbiguous = item.senderInfo.isNameAmbiguous, - roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { item.roomInfo.isDm }, + roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { isDm }, roomDisplayName = item.roomInfo.displayName, isDirect = item.roomInfo.isDirect, - isDm = item.roomInfo.isDm, + isDm = 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 6a88e5051b..7da0f14d14 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, - isDM: Boolean + isOneToOne: Boolean ): Result = withContext(dispatchers.io) { runCatchingExceptions { try { - notificationSettings.await().setDefaultRoomNotificationMode(isEncrypted, isDM, mode.let(RoomNotificationSettingsMapper::mapMode)) + notificationSettings.await().setDefaultRoomNotificationMode(isEncrypted, isOneToOne, 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/oauth/AccountManagementAction.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt similarity index 86% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oauth/AccountManagementAction.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt index 974ae08923..f86c57543a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oauth/AccountManagementAction.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt @@ -6,9 +6,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.matrix.impl.oauth +package io.element.android.libraries.matrix.impl.oidc -import io.element.android.libraries.matrix.api.oauth.AccountManagementAction +import io.element.android.libraries.matrix.api.oidc.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/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt index e75091eadc..ca25eef6ea 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 @@ -44,9 +44,6 @@ import io.element.android.libraries.matrix.impl.mapper.map import io.element.android.libraries.matrix.impl.room.history.map import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest -import io.element.android.libraries.matrix.impl.room.location.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 import io.element.android.libraries.matrix.impl.roomdirectory.map @@ -73,7 +70,6 @@ import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener import org.matrix.rustcomponents.sdk.KnockRequestsListener -import org.matrix.rustcomponents.sdk.LiveLocationException import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType import org.matrix.rustcomponents.sdk.RoomSendQueueUpdate import org.matrix.rustcomponents.sdk.SendQueueListener @@ -348,7 +344,7 @@ class JoinedRustRoom( roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) runCatchingExceptions { val isEncrypted = roomInfoFlow.value.isEncrypted ?: getUpdatedIsEncrypted().getOrThrow() - notificationSettingsService.getRoomNotificationSettings(roomId = roomId, isEncrypted = isEncrypted, isOneToOne = isDm()).getOrThrow() + notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() }.map { roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Ready(it) }.onFailure { @@ -515,34 +511,31 @@ class JoinedRustRoom( } override fun subscribeToLiveLocationShares(): Flow> { - return innerRoom.liveLocationSharesFlow().timedByExpiry(systemClock::epochMillis) + TODO("Not implemented yet") } - 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 sendRawEvent(eventType: String, content: String): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.sendRaw(eventType, content) + Unit } } @@ -550,7 +543,7 @@ class JoinedRustRoom( override fun destroy() { baseRoom.destroy() - liveTimeline.close() + liveInnerTimeline.destroy() threadsListService.destroy() Timber.d("Room $roomId destroyed") } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExt.kt index 4c8da1aca7..668cfc46df 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 { isDm } + .takeIf { isDirect && activeMembersCount.toLong() == 2L } ?.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 a972b3c130..deca0f8ee6 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,7 +43,6 @@ 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 @@ -77,7 +76,6 @@ 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 0551891a6d..e73bed084e 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,6 +23,7 @@ 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 @@ -118,7 +119,7 @@ class RustBaseRoom( innerRoom.membersNoSync().use { members -> members.nextChunk(members.len()) ?.map(RoomMemberMapper::map) - ?.firstOrNull { roomMember -> !roomMember.isServiceMember && roomMember.userId != sessionId && roomMember.membership.isActive() } + ?.firstOrNull { roomMember -> roomMember.userId != sessionId && roomMember.membership.isActive() } } } else { 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..a3af54863c 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 @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapp import io.element.android.libraries.matrix.impl.roomlist.roomOrNull import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.api.inBridgeSdkSpan import io.element.android.services.analytics.api.recordTransaction import io.element.android.services.analyticsproviders.api.recordChildTransaction import io.element.android.services.toolbox.api.systemclock.SystemClock @@ -127,17 +128,19 @@ class RustRoomFactory( val timeline = transaction.recordChildTransaction( operation = "sdkRoom.timelineWithConfiguration", description = "Get timeline from the SDK", - ) { - sdkRoom.timelineWithConfiguration( - TimelineConfiguration( - focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents), - filter = eventFilters?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All, - internalIdPrefix = "live", - dateDividerMode = DateDividerMode.DAILY, - trackReadReceipts = TimelineReadReceiptTracking.ALL_EVENTS, - reportUtds = true, + ) { timelineTransaction -> + analyticsService.inBridgeSdkSpan(parentTraceId = timelineTransaction.traceId()) { + sdkRoom.timelineWithConfiguration( + TimelineConfiguration( + focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents), + filter = eventFilters?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All, + internalIdPrefix = "live", + dateDividerMode = DateDividerMode.DAILY, + trackReadReceipts = TimelineReadReceiptTracking.ALL_EVENTS, + reportUtds = true, + ) ) - ) + } } GetRoomResult.Joined( 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 deleted file mode 100644 index 44be305c02..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/BeaconInfoUpdates.kt +++ /dev/null @@ -1,21 +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.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 deleted file mode 100644 index b10f5cd41c..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationException.kt +++ /dev/null @@ -1,19 +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.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 deleted file mode 100644 index 8e8181539f..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationSharesFlow.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.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 -import io.element.android.libraries.matrix.impl.util.cancelAndDestroy -import kotlinx.coroutines.channels.Channel -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.LiveLocationShareUpdate -import org.matrix.rustcomponents.sdk.LiveLocationsListener -import org.matrix.rustcomponents.sdk.RoomInterface -import org.matrix.rustcomponents.sdk.LiveLocationShare as RustLiveLocationShare - -fun RoomInterface.liveLocationSharesFlow(): Flow> { - fun MutableList.applyUpdate(update: LiveLocationShareUpdate) { - when (update) { - is LiveLocationShareUpdate.Append -> addAll(update.values.map { it.into() }) - is LiveLocationShareUpdate.Clear -> clear() - is LiveLocationShareUpdate.Insert -> add(update.index.toInt(), update.value.into()) - is LiveLocationShareUpdate.PopBack -> if (isNotEmpty()) removeAt(lastIndex) - is LiveLocationShareUpdate.PopFront -> if (isNotEmpty()) removeAt(0) - is LiveLocationShareUpdate.PushBack -> add(update.value.into()) - is LiveLocationShareUpdate.PushFront -> add(0, update.value.into()) - is LiveLocationShareUpdate.Remove -> removeAt(update.index.toInt()) - is LiveLocationShareUpdate.Reset -> { - clear() - addAll(update.values.map { it.into() }) - } - is LiveLocationShareUpdate.Set -> set(update.index.toInt(), update.value.into()) - is LiveLocationShareUpdate.Truncate -> subList(update.length.toInt(), size).clear() - } - } - return callbackFlow { - val observer = liveLocationsObserver() - val shares: MutableList = ArrayList() - val taskHandle = observer.subscribe(object : LiveLocationsListener { - override fun onUpdate(updates: List) { - for (update in updates) { - shares.applyUpdate(update) - } - trySend(shares) - } - }) - awaitClose { - taskHandle.cancelAndDestroy() - observer.destroy() - } - }.buffer(Channel.UNLIMITED) -} - -private fun RustLiveLocationShare.into(): LiveLocationShare { - return LiveLocationShare( - beaconId = EventId(beaconId), - userId = UserId(userId), - lastLocation = lastLocation?.let { - LastLocation( - geoUri = it.location.geoUri, - timestamp = it.ts.toLong(), - assetType = it.location.asset.into(), - ) - }, - startTimestamp = startTs.toLong(), - endTimestamp = (startTs + timeout).toLong(), - ) -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/TimedLiveLocationSharesFlow.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/TimedLiveLocationSharesFlow.kt deleted file mode 100644 index 9bfddf280f..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/TimedLiveLocationSharesFlow.kt +++ /dev/null @@ -1,55 +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.room.location - -import io.element.android.libraries.matrix.api.room.location.LiveLocationShare -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.launch - -/** - * Makes sure to filter and emit live location based on the endTimestamp. - */ -internal fun Flow>.timedByExpiry( - currentTimeMillis: () -> Long = System::currentTimeMillis, -): Flow> = channelFlow { - var timerJob: Job? = null - - fun List.nextExpiryAfter(timestamp: Long): Long? { - return this - .asSequence() - .map { it.endTimestamp } - .filter { it > timestamp } - .minOrNull() - } - - fun List.filterLive(): List { - val currentTimeMillis = currentTimeMillis() - return filter { it.endTimestamp > currentTimeMillis } - } - - fun reschedule(shares: List) { - timerJob?.cancel() - timerJob = launch { - val currentTimeMillis = currentTimeMillis() - val nextExpiry = shares.nextExpiryAfter(currentTimeMillis) ?: return@launch - delay((nextExpiry - currentTimeMillis).coerceAtLeast(0)) - val liveShares = shares.filterLive() - send(liveShares) - reschedule(liveShares) - } - } - - collect { shares -> - val liveShares = shares.filterLive() - send(liveShares) - reschedule(liveShares) - } -} 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 33ecb74ff3..447fa427a6 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,8 +28,7 @@ object RoomMemberMapper { powerLevel = powerLevel, isIgnored = roomMember.isIgnored, role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel), - membershipChangeReason = roomMember.membershipChangeReason, - isServiceMember = roomMember.isServiceMember, + membershipChangeReason = roomMember.membershipChangeReason ) } 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 499868795a..5e2a1c82da 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,8 +26,6 @@ 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 5d8367bb99..27b19ebf19 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.id()}'" + "Set #$index to '${value.displayName()}'" } is RoomListEntriesUpdate.Append -> { - "Append ${values.map { "'" + it.id() + "'" }}" + "Append ${values.map { "'" + it.displayName() + "'" }}" } is RoomListEntriesUpdate.PushBack -> { - "PushBack '${value.id()}'" + "PushBack '${value.displayName()}'" } is RoomListEntriesUpdate.PushFront -> { - "PushFront '${value.id()}'" + "PushFront '${value.displayName()}'" } is RoomListEntriesUpdate.Insert -> { - "Insert at #$index: '${value.id()}'" + "Insert at #$index: '${value.displayName()}'" } is RoomListEntriesUpdate.Remove -> { "Remove #$index" } is RoomListEntriesUpdate.Reset -> { - "Reset all to ${values.map { "'" + it.id() + "'" }}" + "Reset all to ${values.map { "'" + it.displayName() + "'" }}" } 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 afdac3db12..968a768fa2 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,11 +112,6 @@ 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() @@ -131,7 +126,7 @@ class RoomSummaryListProcessor( analyticsService.trackError( IllegalStateException( "Found duplicates in room summaries after a list update from the SDK: $duplicates. " + - "Updates: $updatesDescription" + "Updates: ${updates.description()}" ) ) } 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 cd729d0df1..f83cd648a6 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,7 +36,6 @@ 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/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index d44676d34a..cce3df3de5 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,7 +12,6 @@ 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 @@ -21,7 +20,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.join.JoinRule +import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MsgType @@ -38,8 +37,6 @@ import io.element.android.libraries.matrix.impl.room.location.into import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper -import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterEmptyDayPostProcessor -import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterPublicMembershipChangesPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor @@ -47,7 +44,6 @@ 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 @@ -86,7 +82,7 @@ private const val PAGINATION_SIZE = 50 class RustTimeline( private val inner: InnerTimeline, override val mode: Timeline.Mode, - systemClock: SystemClock, + private val systemClock: SystemClock, private val joinedRoom: JoinedRoom, private val coroutineScope: CoroutineScope, private val dispatcher: CoroutineDispatcher, @@ -125,15 +121,6 @@ class RustTimeline( private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock) private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode) private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode) - private val publicMembershipChangesPostProcessor = FilterPublicMembershipChangesPostProcessor() - private val emptyDayPostProcessor = FilterEmptyDayPostProcessor() - - 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) @@ -234,15 +221,14 @@ class RustTimeline( _timelineItems, backwardPaginationStatus, forwardPaginationStatus, - joinedRoom.roomInfoFlow.map { RoomTimelineInfo(it.creators, it.isDm, it.joinRule, it.isEncrypted) }.distinctUntilChanged(), + joinedRoom.roomInfoFlow.map { it.creators to it.isDm }.distinctUntilChanged(), ) { timelineItems, backwardPaginationStatus, forwardPaginationStatus, - roomInfo, + (roomCreators, isDm), -> withContext(dispatcher) { - val (roomCreators, isDm, joinRule, isEncrypted) = roomInfo timelineItems .let { items -> roomBeginningPostProcessor.process( @@ -252,18 +238,6 @@ class RustTimeline( hasMoreToLoadBackwards = backwardPaginationStatus.hasMoreToLoad, ) } - // This should be the first post processor after room beginning. - .let { items -> - publicMembershipChangesPostProcessor.process( - items = items, - joinRule = joinRule, - isEncrypted = isEncrypted, - ) - } - // After removing public membership changes, we might end up with empty days, so we need to filter them out. - .let { items -> - emptyDayPostProcessor.process(items) - } .let { items -> loadingIndicatorsPostProcessor.process( items = items, @@ -318,6 +292,20 @@ class RustTimeline( } } + /** + * Send a raw/custom event to the room. + * + * The Rust SDK moved raw event sending from Timeline to Room between + * 26.03.x and 26.04.x, so we proxy through the owning JoinedRoom. + * + * @param eventType The event type (e.g., "co.sulkta.payment.request") + * @param content The JSON content of the event + */ + override suspend fun sendRaw( + eventType: String, + content: String, + ): Result = joinedRoom.sendRawEvent(eventType, content) + override suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result = withContext(dispatcher) { runCatchingExceptions { inner.redactEvent( 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 fa671bc546..763a3c4184 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt @@ -11,8 +11,6 @@ package io.element.android.libraries.matrix.impl.timeline.item.event import io.element.android.libraries.architecture.AsyncData 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.CallIntent -import io.element.android.libraries.matrix.api.room.location.LiveLocationInfo import io.element.android.libraries.matrix.api.timeline.item.EmbeddedEventInfo import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary @@ -21,7 +19,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent -import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.PollContent @@ -32,17 +29,17 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.poll.map import io.element.android.libraries.matrix.impl.room.join.map -import io.element.android.libraries.matrix.impl.room.location.into import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap -import org.matrix.rustcomponents.sdk.BeaconInfo import org.matrix.rustcomponents.sdk.EmbeddedEventDetails import org.matrix.rustcomponents.sdk.MsgLikeContent import org.matrix.rustcomponents.sdk.MsgLikeKind +import org.matrix.rustcomponents.sdk.MessageLikeEventType import org.matrix.rustcomponents.sdk.TimelineItemContent import org.matrix.rustcomponents.sdk.use import uniffi.matrix_sdk_ui.RoomPinnedEventsChange @@ -113,16 +110,17 @@ class TimelineEventContentMapper( ) } is MsgLikeKind.LiveLocation -> { - LiveLocationContent( - isLive = kind.content.isLive, - startTimestamp = kind.content.ts.toLong(), - description = kind.content.description, - timeout = kind.content.timeoutMs.toLong(), - assetType = kind.content.assetType.into(), - locations = kind.content.locations.map { location -> location.map() } + // Live location messages are a special kind of message that we want to treat as unknown content for now + UnknownContent + } + is MsgLikeKind.Other -> { + // MsgLikeKind.Other contains custom event types + // Pass through the event type so downstream handlers can process it + CustomEventContent( + eventType = (kind.eventType as? MessageLikeEventType.Other)?.v1 ?: kind.eventType.toString(), + rawJson = null, // Raw JSON accessed via TimelineItemDebugInfoProvider ) } - is MsgLikeKind.Other -> UnknownContent } } is TimelineItemContent.ProfileChange -> { @@ -148,14 +146,7 @@ class TimelineEventContentMapper( ) } is TimelineItemContent.CallInvite -> LegacyCallInviteContent - is TimelineItemContent.RtcNotification -> CallNotifyContent( - callIntent = if (it.callIntent == "audio") { - CallIntent.AUDIO - } else { - CallIntent.VIDEO - }, - declinedBy = it.declinedBy.map(::UserId) - ) + is TimelineItemContent.RtcNotification -> CallNotifyContent } } } @@ -277,11 +268,3 @@ private fun RustEncryptedMessage.map(): UnableToDecryptContent.Data { RustEncryptedMessage.Unknown -> UnableToDecryptContent.Data.Unknown } } - -private fun BeaconInfo.map(): LiveLocationInfo { - return LiveLocationInfo( - description = description, - geoUri = geoUri, - timestamp = ts.toLong(), - ) -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessor.kt deleted file mode 100644 index b7dceafa25..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessor.kt +++ /dev/null @@ -1,38 +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.timeline.postprocessor - -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem - -/** - * Post-processor to filter out day separators for days that don't contain any events. - */ -class FilterEmptyDayPostProcessor { - /** - * Filters out day separators from [items] for days that don't contain any events. - */ - fun process(items: List): List = buildList { - // The timeline is ordered by descending timestamp, so events that happened during a day appear before the day separator for that day. - // We can use this to determine if a day separator should be kept or not. - var hasEvent = false - for (item in items) { - if (item is MatrixTimelineItem.Event) { - hasEvent = true - add(item) - } else if (item is MatrixTimelineItem.Virtual && item.virtual is VirtualTimelineItem.DayDivider) { - if (hasEvent) { - add(item) - hasEvent = false - } - } else { - add(item) - } - } - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessor.kt deleted file mode 100644 index 806c9369bf..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessor.kt +++ /dev/null @@ -1,43 +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.timeline.postprocessor - -import io.element.android.libraries.matrix.api.room.join.JoinRule -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange -import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent -import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent - -/** - * Post-processor to filter out public membership changes for non-encrypted, publicly joinable rooms. - */ -class FilterPublicMembershipChangesPostProcessor { - /** - * Filters out public membership changes from [items] if the room is publicly joinable and not encrypted. - */ - fun process( - items: List, - joinRule: JoinRule?, - isEncrypted: Boolean?, - ): List { - return if (joinRule !is JoinRule.Invite && isEncrypted == false) { - filterMembershipEvents(items) - } else { - items - } - } - - private fun filterMembershipEvents(items: List): List = items.filter { item -> - val eventContent = (item as? MatrixTimelineItem.Event)?.event?.content ?: return@filter true - when (eventContent) { - is RoomMembershipContent -> eventContent.change != null && eventContent.change !in listOf(MembershipChange.JOINED, MembershipChange.LEFT) - is ProfileChangeContent -> false - else -> true - } - } -} 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 8991d26f9c..397280231d 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. For rooms that aren't invite-only and aren't encrypted, it also removes join/leave and profile change events. + * or add the RoomBeginning item. */ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { fun process( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt index cad3c83443..dc3f8c305b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt @@ -17,7 +17,6 @@ import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.TracingConfiguration import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration -import org.matrix.rustcomponents.sdk.SentryConfig import org.matrix.rustcomponents.sdk.TracingFileConfiguration import org.matrix.rustcomponents.sdk.reloadTracingFileWriter import timber.log.Timber @@ -60,17 +59,16 @@ private fun WriteToFilesConfiguration.toTracingFileConfiguration(): TracingFileC } } -fun TracingConfiguration.map(buildMeta: BuildMeta): org.matrix.rustcomponents.sdk.TracingConfiguration = org.matrix.rustcomponents.sdk.TracingConfiguration( - writeToStdoutOrSystem = writesToLogcat, - logLevel = logLevel.toRustLogLevel(), - extraTargets = extraTargets, - traceLogPacks = traceLogPacks.map(), - writeToFiles = writesToFilesConfiguration.toTracingFileConfiguration(), - sentryConfig = sdkSentryDsn?.let { - SentryConfig( - dsn = it, - appVersion = buildMeta.versionName, - appPlatform = "Android", - ) - } -) +@Suppress("UNUSED_PARAMETER") +fun TracingConfiguration.map(buildMeta: BuildMeta): org.matrix.rustcomponents.sdk.TracingConfiguration { + // Note: sdkSentryDsn was removed; the SDK now takes an optional SentryConfig + // object which we don't use. Passing null opts out of SDK-side Sentry. + return org.matrix.rustcomponents.sdk.TracingConfiguration( + writeToStdoutOrSystem = writesToLogcat, + logLevel = logLevel.toRustLogLevel(), + extraTargets = extraTargets, + traceLogPacks = traceLogPacks.map(), + writeToFiles = writesToFilesConfiguration.toTracingFileConfiguration(), + sentryConfig = null, + ) +} 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 f5df21008b..815e134cf2 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,8 +16,7 @@ private val sha256 by lazy { MessageDigest.getInstance("SHA-256") } @OptIn(ExperimentalStdlibApi::class) private fun anonymizeToken(token: String): String { - // 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) + return sha256.digest(token.toByteArray()).toHexString() } 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 b2a13f5f1a..7fb2935897 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 requestDeviceVerification() = tryOrFail { + override suspend fun requestCurrentSessionVerification() = tryOrFail { ensureEncryptionIsInitialized() verificationController.requestDeviceVerification() currentVerificationRequest = VerificationRequest.Outgoing.CurrentSession @@ -146,7 +146,7 @@ class RustSessionVerificationService( override suspend fun declineVerification() = tryOrFail { verificationController.declineVerification() } - override suspend fun startSasVerification() = tryOrFail { + override suspend fun startVerification() = tryOrFail { verificationController.startSasVerification() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/MatrixAccountDataWalletSecretStorage.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/MatrixAccountDataWalletSecretStorage.kt new file mode 100644 index 0000000000..8f8c3fef3b --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/MatrixAccountDataWalletSecretStorage.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.libraries.matrix.impl.walletsecretstorage + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.walletsecretstorage.WalletSecretStorage +import io.element.android.libraries.matrix.api.walletsecretstorage.WalletSecretStorageException +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.Client +import timber.log.Timber + +/** + * Implementation of [WalletSecretStorage] that persists the encrypted + * wallet seed envelope as Matrix account data. + * + * We store under our own namespace (`com.sulkta.wallet.seed.v1`) so we're + * NOT impersonating Matrix-spec secret storage. The blob is opaque JSON + * produced by [WalletSecretEnvelope]; the homeserver just holds bytes. + */ +class MatrixAccountDataWalletSecretStorage( + private val client: Client, + private val dispatchers: CoroutineDispatchers, +) : WalletSecretStorage { + + override suspend fun putSeed(recoveryKey: String, seedPhrase: String): Result = + withContext(dispatchers.io) { + runCatching { + val key = RecoveryKeyDerivation.deriveKey(recoveryKey) + ?: throw WalletSecretStorageException.InvalidRecoveryKey + val envelope = WalletSecretEnvelope.seal( + key = key, + aad = SLOT, + plaintext = seedPhrase.toByteArray(Charsets.UTF_8), + ) + try { + client.setAccountData(SLOT, envelope) + } catch (e: Exception) { + Timber.w(e, "[WalletSecretStorage] setAccountData failed for $SLOT") + throw WalletSecretStorageException.WriteFailed + } + } + } + + override suspend fun getSeed(recoveryKey: String): Result = + withContext(dispatchers.io) { + runCatching { + val key = RecoveryKeyDerivation.deriveKey(recoveryKey) + ?: throw WalletSecretStorageException.InvalidRecoveryKey + + val envelopeJson = fetchEnvelopeJson() ?: return@runCatching null + val plaintext = WalletSecretEnvelope.open( + key = key, + expectedAad = SLOT, + envelopeJson = envelopeJson, + ) + plaintext?.toString(Charsets.UTF_8) + } + } + + override suspend fun hasSeedBackup(): Result = + withContext(dispatchers.io) { + runCatching { fetchEnvelopeJson() != null } + } + + override suspend fun deleteSeed(): Result = + withContext(dispatchers.io) { + runCatching { + // Matrix has no dedicated delete; setting empty object is the + // idiomatic "remove" — future reads see it as absent/empty. + client.setAccountData(SLOT, "{}") + } + } + + /** + * Read the raw envelope JSON from account data, or null if the slot is + * absent or holds an empty/tombstone value. Absorbs SDK-thrown errors + * that indicate "not found" into a null return. + */ + private suspend fun fetchEnvelopeJson(): String? { + val raw = try { + client.accountData(SLOT) + } catch (e: Exception) { + // The Rust SDK surfaces "not found" as a ClientException; we + // don't want to leak that to callers — absence is normal state. + Timber.d("[WalletSecretStorage] accountData($SLOT) missing: ${e.javaClass.simpleName}") + return null + } + if (raw.isNullOrBlank()) return null + // deleteSeed() writes "{}" as a tombstone; treat that as absent. + if (raw.trim() == "{}") return null + return raw + } + + companion object { + /** + * Storage slot name. Must stay stable across app versions — every + * backup ever written uses this as the AAD too, so changing it + * would orphan existing blobs. + */ + const val SLOT = "com.sulkta.wallet.seed.v1" + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/RecoveryKeyDerivation.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/RecoveryKeyDerivation.kt new file mode 100644 index 0000000000..16c01c38c7 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/RecoveryKeyDerivation.kt @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.libraries.matrix.impl.walletsecretstorage + +import java.math.BigInteger +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +/** + * Derives our wallet-encryption key from the Matrix recovery key the user + * already has from setting up Matrix crypto backup. + * + * Pipeline: + * user_input ─ strip whitespace ─▶ base58 decode ─▶ 35 bytes + * │ + * verify prefix (0x8b 0x01) + parity ─┘ + * │ + * 32 bytes of entropy + * │ + * HKDF-SHA256(ikm=entropy, info="sulkta.wallet.seed.v1", len=32) + * │ + * 32-byte wallet AES key + * + * The HKDF info label is what guarantees we never derive the same key + * bytes Matrix uses for its own crypto — Matrix derives with different + * labels (e.g. "m.megolm_backup.v1"), we use our own namespace. + */ +internal object RecoveryKeyDerivation { + /** Two-byte prefix Matrix spec mandates at the start of decoded recovery keys. */ + private val MATRIX_RECOVERY_KEY_PREFIX = byteArrayOf(0x8b.toByte(), 0x01.toByte()) + private const val PREFIX_LENGTH = 2 + private const val ENTROPY_LENGTH = 32 + private const val PARITY_LENGTH = 1 + private const val DECODED_LENGTH = PREFIX_LENGTH + ENTROPY_LENGTH + PARITY_LENGTH + + /** Label binding derived keys to this slot/purpose. Change = incompatible with prior blobs. */ + const val WALLET_SEED_KEY_LABEL = "sulkta.wallet.seed.v1" + private const val DERIVED_KEY_LENGTH = 32 + + /** + * Derive a 32-byte AES key from the user's recovery key string for a + * given HKDF info label. Returns null if the recovery key is malformed + * (bad base58, wrong prefix, bad parity, wrong length). + */ + fun deriveKey(recoveryKey: String, infoLabel: String = WALLET_SEED_KEY_LABEL): ByteArray? { + val entropy = parseRecoveryKey(recoveryKey) ?: return null + return hkdfSha256( + ikm = entropy, + salt = ByteArray(0), + info = infoLabel.toByteArray(Charsets.UTF_8), + length = DERIVED_KEY_LENGTH, + ) + } + + // ── recovery key parsing ────────────────────────────────────────────── + + private fun parseRecoveryKey(input: String): ByteArray? { + val normalized = input.replace("\\s".toRegex(), "") + if (normalized.isEmpty()) return null + val decoded = base58Decode(normalized) ?: return null + if (decoded.size != DECODED_LENGTH) return null + if (decoded[0] != MATRIX_RECOVERY_KEY_PREFIX[0] || decoded[1] != MATRIX_RECOVERY_KEY_PREFIX[1]) return null + + // Parity byte is XOR of all preceding bytes. + var parity: Byte = 0 + for (i in 0 until decoded.size - 1) parity = (parity.toInt() xor decoded[i].toInt()).toByte() + if (parity != decoded[decoded.size - 1]) return null + + return decoded.copyOfRange(MATRIX_RECOVERY_KEY_PREFIX.size, MATRIX_RECOVERY_KEY_PREFIX.size + ENTROPY_LENGTH) + } + + /** + * Test-only: construct a spec-valid recovery-key string from 32 bytes + * of entropy. Used by unit tests to build fixtures without pasting + * magic strings we can't verify by eye. + */ + internal fun encodeRecoveryKeyForTesting(entropy: ByteArray): String { + require(entropy.size == ENTROPY_LENGTH) { "entropy must be $ENTROPY_LENGTH bytes" } + val raw = ByteArray(DECODED_LENGTH) + raw[0] = MATRIX_RECOVERY_KEY_PREFIX[0] + raw[1] = MATRIX_RECOVERY_KEY_PREFIX[1] + System.arraycopy(entropy, 0, raw, MATRIX_RECOVERY_KEY_PREFIX.size, ENTROPY_LENGTH) + var parity: Byte = 0 + for (i in 0 until raw.size - 1) parity = (parity.toInt() xor raw[i].toInt()).toByte() + raw[raw.size - 1] = parity + return base58Encode(raw) + } + + // ── Base58 (Bitcoin alphabet, per MSC3732) ──────────────────────────── + + private const val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + private val ALPHABET_INDEX: IntArray = IntArray(128) { -1 }.also { arr -> + ALPHABET.forEachIndexed { i, ch -> arr[ch.code] = i } + } + + private fun base58Encode(input: ByteArray): String { + if (input.isEmpty()) return "" + var leadingZeros = 0 + while (leadingZeros < input.size && input[leadingZeros] == 0.toByte()) leadingZeros++ + + // Treat input as unsigned by prefixing a zero — BigInteger is two's-complement. + val num = BigInteger(1, input) + val sb = StringBuilder() + var n = num + val base = BigInteger.valueOf(58) + while (n.signum() > 0) { + val divRem = n.divideAndRemainder(base) + sb.append(ALPHABET[divRem[1].toInt()]) + n = divRem[0] + } + repeat(leadingZeros) { sb.append(ALPHABET[0]) } + return sb.reverse().toString() + } + + private fun base58Decode(input: String): ByteArray? { + if (input.isEmpty()) return ByteArray(0) + var leadingZeros = 0 + while (leadingZeros < input.length && input[leadingZeros] == '1') leadingZeros++ + + var num = BigInteger.ZERO + val base = BigInteger.valueOf(58) + for (i in 0 until input.length) { + val ch = input[i] + if (ch.code >= ALPHABET_INDEX.size) return null + val digit = ALPHABET_INDEX[ch.code] + if (digit < 0) return null + num = num.multiply(base).add(BigInteger.valueOf(digit.toLong())) + } + + val bytes = num.toByteArray() + // BigInteger.toByteArray may prepend a zero byte to keep the result positive; trim it. + val trimmed = if (bytes.isNotEmpty() && bytes[0] == 0.toByte() && bytes.size > 1) bytes.copyOfRange(1, bytes.size) else bytes + + val out = ByteArray(leadingZeros + trimmed.size) + System.arraycopy(trimmed, 0, out, leadingZeros, trimmed.size) + return out + } + + // ── HKDF-SHA256 (RFC 5869) ──────────────────────────────────────────── + + private fun hkdfSha256(ikm: ByteArray, salt: ByteArray, info: ByteArray, length: Int): ByteArray { + val mac = Mac.getInstance("HmacSHA256") + // Extract + val actualSalt = if (salt.isEmpty()) ByteArray(mac.macLength) else salt + mac.init(SecretKeySpec(actualSalt, "HmacSHA256")) + val prk = mac.doFinal(ikm) + + // Expand + mac.init(SecretKeySpec(prk, "HmacSHA256")) + val hashLen = mac.macLength + val n = (length + hashLen - 1) / hashLen + require(n <= 255) { "HKDF output too long" } + + val okm = ByteArray(length) + var prev = ByteArray(0) + var written = 0 + for (i in 1..n) { + mac.reset() + mac.update(prev) + mac.update(info) + mac.update(i.toByte()) + prev = mac.doFinal() + val take = minOf(hashLen, length - written) + System.arraycopy(prev, 0, okm, written, take) + written += take + } + return okm + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/WalletSecretEnvelope.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/WalletSecretEnvelope.kt new file mode 100644 index 0000000000..076d30a7a7 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/WalletSecretEnvelope.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.libraries.matrix.impl.walletsecretstorage + +import android.util.Base64 +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.SecretKeySpec + +/** + * A self-contained authenticated-encryption envelope for arbitrary bytes + * stored in Matrix account data. + * + * We deliberately do NOT reuse the Matrix `m.secret_storage.v1.aes-hmac-sha2` + * format. That format is defined by Matrix spec for Matrix-managed secrets + * (cross-signing keys, megolm backup). We are storing our own application + * secrets in a namespace we own; using our own format makes it explicit + * that we're not impersonating Matrix secret storage. + * + * Format written to account data (UTF-8 JSON): + * + * { + * "v": 1, // envelope version + * "alg": "aes-256-gcm", // algorithm tag + * "iv": "", // GCM nonce + * "ct": "",// GCM output + * "aad": "com.sulkta.wallet.seed.v1" // authenticated context + * } + * + * The `aad` binds the envelope to its storage slot so a blob can't be + * lifted from one slot and successfully decrypted in another. + */ +internal object WalletSecretEnvelope { + private const val ENVELOPE_VERSION = 1 + private const val ALGORITHM = "aes-256-gcm" + private const val CIPHER_TRANSFORMATION = "AES/GCM/NoPadding" + private const val GCM_TAG_LENGTH_BITS = 128 + private const val GCM_IV_LENGTH_BYTES = 12 + private const val AES_KEY_LENGTH_BYTES = 32 + + private val json = Json { ignoreUnknownKeys = true } + private val secureRandom = SecureRandom() + + @Serializable + private data class Envelope( + @SerialName("v") val version: Int, + @SerialName("alg") val algorithm: String, + @SerialName("iv") val ivB64: String, + @SerialName("ct") val ciphertextB64: String, + @SerialName("aad") val aad: String, + ) + + /** + * Seal [plaintext] under [key], binding the result to [aad]. + * Returns the JSON-serialized envelope. + */ + fun seal(key: ByteArray, aad: String, plaintext: ByteArray): String { + require(key.size == AES_KEY_LENGTH_BYTES) { "key must be $AES_KEY_LENGTH_BYTES bytes" } + + val iv = ByteArray(GCM_IV_LENGTH_BYTES).also(secureRandom::nextBytes) + val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION).apply { + init( + Cipher.ENCRYPT_MODE, + SecretKeySpec(key, "AES"), + GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv), + ) + updateAAD(aad.toByteArray(Charsets.UTF_8)) + } + val ciphertext = cipher.doFinal(plaintext) + + val envelope = Envelope( + version = ENVELOPE_VERSION, + algorithm = ALGORITHM, + ivB64 = Base64.encodeToString(iv, Base64.NO_WRAP), + ciphertextB64 = Base64.encodeToString(ciphertext, Base64.NO_WRAP), + aad = aad, + ) + return json.encodeToString(Envelope.serializer(), envelope) + } + + /** + * Open a JSON envelope. Returns null on any integrity, parse, or + * version failure — callers should treat null as "unreadable", + * not leaking the exact reason. + * + * Throws [IllegalArgumentException] only if [key] is the wrong size — + * that's a caller bug, not a data-integrity signal. + */ + fun open(key: ByteArray, expectedAad: String, envelopeJson: String): ByteArray? { + require(key.size == AES_KEY_LENGTH_BYTES) { "key must be $AES_KEY_LENGTH_BYTES bytes" } + return try { + val envelope = json.decodeFromString(Envelope.serializer(), envelopeJson) + if (envelope.version != ENVELOPE_VERSION) return null + if (envelope.algorithm != ALGORITHM) return null + if (envelope.aad != expectedAad) return null + + val iv = Base64.decode(envelope.ivB64, Base64.NO_WRAP) + val ciphertext = Base64.decode(envelope.ciphertextB64, Base64.NO_WRAP) + if (iv.size != GCM_IV_LENGTH_BYTES) return null + + val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION).apply { + init( + Cipher.DECRYPT_MODE, + SecretKeySpec(key, "AES"), + GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv), + ) + updateAAD(expectedAad.toByteArray(Charsets.UTF_8)) + } + cipher.doFinal(ciphertext) + } catch (e: javax.crypto.AEADBadTagException) { + // Wrong key, tampered ciphertext, or AAD mismatch — all surface the same way. + null + } catch (e: Exception) { + // Malformed JSON, bad base64, etc. + null + } + } +} 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 0036f2f962..6aa3ef0e5b 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,11 +16,15 @@ 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 { @@ -39,6 +43,7 @@ class RustClientSessionDelegateTest { refreshToken = "rt", ) ) + runCurrent() val result = sessionStore.getLatestSession() assertThat(result!!.accessToken).isEqualTo("at") assertThat(result.refreshToken).isEqualTo("rt") @@ -75,4 +80,5 @@ 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/analytics/JoinedExtKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedExtKtTest.kt index 3c094f3581..68adfe00a2 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,19 +43,6 @@ 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, @@ -93,13 +80,12 @@ class JoinedExtKtTest { } private fun aRoom( - isDm: Boolean = false, isDirect: Boolean = false, isSpace: Boolean = false, joinedMemberCount: Long = 0 ): FakeBaseRoom { return FakeBaseRoom().apply { - givenRoomInfo(aRoomInfo(isDm = isDm, isDirect = isDirect, isSpace = isSpace, joinedMembersCount = joinedMemberCount)) + givenRoomInfo(aRoomInfo(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 cc5fb4a394..8449d9a6c3 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.OAuthException +import org.matrix.rustcomponents.sdk.OidcException class AuthenticationExceptionMappingTest { @Test @@ -64,17 +64,17 @@ class AuthenticationExceptionMappingTest { } @Test - 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") + 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") } private inline fun ThrowableSubject.isException(message: String) { 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 63f573536c..a5c7b2dbc8 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, - supportsOAuthLogin = false + supportsOidcLogin = false ) // When @@ -31,7 +31,7 @@ class HomeserverDetailsKtTest { MatrixHomeServerDetails( url = "https://example.org", supportsPasswordLogin = true, - supportsOAuthLogin = false + supportsOidcLogin = false ) ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OAuthConfigurationProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt similarity index 76% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OAuthConfigurationProviderTest.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt index 3b54f2d1f3..095cf54946 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OAuthConfigurationProviderTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.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.FakeOAuthRedirectUrlProvider +import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider import io.element.android.libraries.matrix.test.core.aBuildMeta import org.junit.Test -class OAuthConfigurationProviderTest { +class OidcConfigurationProviderTest { @Test fun get() { - val result = OAuthConfigurationProvider( + val result = OidcConfigurationProvider( buildMeta = aBuildMeta( applicationName = "myName", ), - oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(), + oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), ).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 903273113b..50d1f3723b 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 OAuth login`() = runTest { - val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsOAuthLogin = true) } + fun `check - is valid if it supports OIDC login`() = runTest { + val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsOidcLogin = true) } assertThat(sut.check("https://matrix.host.org").getOrNull()).isTrue() } 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 7f422acfcf..f4ce7b1fdd 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,8 +9,6 @@ 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 @@ -18,7 +16,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.FakeOAuthRedirectUrlProvider +import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider 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 @@ -52,7 +50,6 @@ class RustMatrixAuthenticationServiceTest { private fun TestScope.createRustMatrixAuthenticationService( sessionStore: SessionStore = InMemorySessionStore(), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), - enterpriseService: EnterpriseService = FakeEnterpriseService(), ): RustMatrixAuthenticationService { val baseDirectory = File("/base") val cacheDirectory = File("/cache") @@ -67,11 +64,10 @@ class RustMatrixAuthenticationServiceTest { sessionStore = sessionStore, rustMatrixClientFactory = rustMatrixClientFactory, passphraseGenerator = FakePassphraseGenerator(), - oAuthConfigurationProvider = OAuthConfigurationProvider( + oidcConfigurationProvider = OidcConfigurationProvider( buildMeta = aBuildMeta(), - oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(), + oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), ), - 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 4de5e55985..0ef20c82a6 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.OAuthMetadataInvalid())).isEqualTo(QrLoginException.OAuthMetadataInvalid) + assertThat(QrErrorMapper.map(RustHumanQrLoginException.OidcMetadataInvalid())).isEqualTo(QrLoginException.OidcMetadataInvalid) 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 63ad77238b..4db2db7107 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,7 +9,6 @@ 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 @@ -67,11 +66,8 @@ 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, @@ -80,11 +76,8 @@ 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 cb5e221dc9..1b0cc12461 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,9 +62,6 @@ 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, @@ -104,7 +101,4 @@ 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 6b4f958d84..77fa814cca 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,7 +24,6 @@ internal fun aRustRoomMember( isIgnored: Boolean = false, role: RoomMemberRole = RoomMemberRole.USER, membershipChangeReason: String? = null, - isServiceMember: Boolean = false, ) = RoomMember( userId = userId.value, displayName = displayName, @@ -35,5 +34,4 @@ 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 28af0093f6..1c1bbb42e3 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,8 +22,6 @@ internal fun aRustRoomPowerLevelsValues( roomAvatar: Long, roomTopic: Long, spaceChild: Long, - beacon: Long, - beaconInfo: Long, ) = RoomPowerLevelsValues( ban = ban, invite = invite, @@ -35,7 +33,5 @@ internal fun aRustRoomPowerLevelsValues( roomName = roomName, roomAvatar = roomAvatar, roomTopic = roomTopic, - spaceChild = spaceChild, - beacon = beacon, - beaconInfo = beaconInfo, + spaceChild = spaceChild ) 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 af7f44597c..4671c457b0 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, - oauthData = null, + oidcData = 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 2eb46eddaf..50115055c2 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,7 +19,6 @@ 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 = "", @@ -36,7 +35,6 @@ internal fun aRustSpaceRoom( ) = SpaceRoom( roomId = roomId.value, isDirect = isDirect, - isDm = isDm, canonicalAlias = canonicalAlias, rawName = rawName, displayName = displayName, @@ -50,5 +48,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/FakeFfiClientBuilder.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt index 623fe9a8cf..59d82487b9 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,7 +17,6 @@ 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 @@ -48,6 +47,5 @@ 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 0899b16325..cd0733695b 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: suspend () -> Unit = {}, - private val scanResult: suspend (QrCodeData) -> Unit = {}, + private val generateResult: () -> Unit = {}, + private val scanResult: (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 5351fbc8df..ade3a2328f 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 supportsOAuthLogin: Boolean = false, + private val supportsOidcLogin: Boolean = false, private val supportsSsoLogin: Boolean = false, ) : HomeserverLoginDetails(NoHandle) { override fun url(): String = url - override fun supportsOauthLogin(): Boolean = supportsOAuthLogin + override fun supportsOidcLogin(): Boolean = supportsOidcLogin 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 985dc935ff..c47c4406b6 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,6 +32,4 @@ 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 56cc5b9a1f..c0ecc53d4f 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 suspend fun rooms(): List { + override fun rooms(): List { return roomsResult() } @@ -53,7 +53,7 @@ class FakeFfiSpaceRoomList( spaceRoomListPaginationStateListener?.onUpdate(state) } - override suspend fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle { + override fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle { spaceRoomListEntriesListener = listener return FakeFfiTaskHandle() } 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 fd635e2da6..a180e4d515 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,7 +15,6 @@ 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 @@ -30,13 +29,7 @@ import org.matrix.rustcomponents.sdk.QrCodeDecodeException class RustLinkDesktopHandlerTest { @Test fun `handleScannedQrCode function works as expected`() = runTest { - val completable = CompletableDeferred() - val handler = FakeFfiGrantLoginWithQrCodeHandler( - scanResult = { - // Ensure that the coroutine is hold - completable.await() - } - ) + val handler = FakeFfiGrantLoginWithQrCodeHandler() val sut = createRustLinkDesktopHandler( handler, ) @@ -60,36 +53,6 @@ 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 ad751cc86b..aa13996e8a 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,7 +17,6 @@ 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 @@ -31,13 +30,7 @@ import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException class RustLinkMobileHandlerTest { @Test fun `start function works as expected`() = runTest { - val completable = CompletableDeferred() - val handler = FakeFfiGrantLoginWithQrCodeHandler( - generateResult = { - // Ensure that the coroutine is hold - completable.await() - } - ) + val handler = FakeFfiGrantLoginWithQrCodeHandler() val sut = createRustLinkMobileHandler( handler, ) @@ -63,36 +56,6 @@ 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) } } @@ -118,35 +81,6 @@ 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) - } - } - 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 fb9ec6625b..e5fc8b154f 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 @@ -36,7 +36,7 @@ class SessionKtTest { assertThat(result.refreshToken).isEqualTo("refreshToken") assertThat(result.homeserverUrl).isEqualTo(A_HOMESERVER_URL) assertThat(result.isTokenValid).isTrue() - assertThat(result.oAuthData).isNull() + assertThat(result.oidcData).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.oAuthData).isNull() + assertThat(result.oidcData).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/oauth/AccountManagementActionKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementActionKtTest.kt similarity index 90% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oauth/AccountManagementActionKtTest.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementActionKtTest.kt index 1495716e87..3637ef78cc 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oauth/AccountManagementActionKtTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementActionKtTest.kt @@ -6,10 +6,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.matrix.impl.oauth +package io.element.android.libraries.matrix.impl.oidc import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.oauth.AccountManagementAction +import io.element.android.libraries.matrix.api.oidc.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/room/RoomInfoExtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExtTest.kt index dd91595359..86a50c3926 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,7 +20,8 @@ class RoomInfoExtTest { @Test fun `get non empty element Heroes`() { val result = aRustRoomInfo( - isDm = true, + isDirect = true, + activeMembersCount = 2uL, heroes = listOf(aRustRoomHero()) ).elementHeroes() assertThat(result).isEqualTo( @@ -37,7 +38,8 @@ class RoomInfoExtTest { @Test fun `too many heroes and element Heroes is empty`() { val result = aRustRoomInfo( - isDm = true, + isDirect = true, + activeMembersCount = 2uL, heroes = listOf(aRustRoomHero(), aRustRoomHero()) ).elementHeroes() assertThat(result).isEmpty() @@ -46,7 +48,18 @@ class RoomInfoExtTest { @Test fun `not direct and element Heroes is empty`() { val result = aRustRoomInfo( - isDm = false, + 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, 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 dc3b16ca3c..ab353bc0f5 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,8 +86,6 @@ class RoomInfoMapperTest { privilegedCreatorsRole = true, isLowPriority = true, activeRoomCallConsensusIntent = RtcCallIntentConsensus.Full(RtcCallIntent.AUDIO), - isDm = true, - fullyReadEventId = AN_EVENT_ID.value, ) ) ).isEqualTo( @@ -138,8 +136,6 @@ class RoomInfoMapperTest { privilegedCreatorRole = true, isLowPriority = true, activeCallIntentConsensus = CallIntentConsensus.Full(CallIntent.AUDIO), - isDm = true, - fullyReadEventId = AN_EVENT_ID, ) ) } @@ -185,8 +181,6 @@ class RoomInfoMapperTest { privilegedCreatorsRole = true, isLowPriority = true, activeRoomCallConsensusIntent = RtcCallIntentConsensus.None, - isDm = false, - fullyReadEventId = AN_EVENT_ID.value, ) ) ).isEqualTo( @@ -231,8 +225,6 @@ 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 deleted file mode 100644 index ba91dae468..0000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/location/TimedLiveLocationSharesFlowTest.kt +++ /dev/null @@ -1,136 +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.room.location - -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 -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runTest -import org.junit.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class TimedLiveLocationSharesFlowTest { - @Test - fun `it keeps emitting shares for subsequent expiries without upstream changes`() = runTest { - val shares = listOf( - 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) - .timedByExpiry(currentTimeMillis = { testScheduler.currentTime }) - .test { - assertThat(awaitItem()).isEqualTo(shares) - - advanceTimeBy(1_000) - assertThat(awaitItem()).isEqualTo(shares.drop(1)) - - advanceTimeBy(999) - expectNoEvents() - - advanceTimeBy(1) - assertThat(awaitItem()).isEqualTo(shares.drop(2)) - - advanceTimeBy(999) - expectNoEvents() - - advanceTimeBy(1) - assertThat(awaitItem()).isEmpty() - - awaitComplete() - } - } - - @Test - fun `it does not double-emit when a share is already expired on receipt`() = runTest { - val shares = listOf( - aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 500), - aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 2_000), - ) - - flowOf(shares) - .timedByExpiry(currentTimeMillis = { 1_000 + testScheduler.currentTime }) - .test { - assertThat(awaitItem()).isEqualTo(shares.drop(1)) - expectNoEvents() - - advanceTimeBy(999) - expectNoEvents() - - advanceTimeBy(1) - assertThat(awaitItem()).isEmpty() - - awaitComplete() - } - } - - @Test - fun `it reschedules timed emission when upstream shares change`() = runTest { - val upstream = MutableSharedFlow>(extraBufferCapacity = 1) - val initialShares = listOf(aLiveLocationShare(endTimestamp = 10_000)) - val updatedShares = listOf( - aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 10_000), - aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 6_000), - ) - - upstream - .timedByExpiry(currentTimeMillis = { testScheduler.currentTime }) - .test { - upstream.emit(initialShares) - assertThat(awaitItem()).isEqualTo(initialShares) - - advanceTimeBy(5_000) - upstream.emit(updatedShares) - assertThat(awaitItem()).isEqualTo(updatedShares) - - advanceTimeBy(999) - expectNoEvents() - - advanceTimeBy(1) - assertThat(awaitItem()).isEqualTo(updatedShares.take(1)) - - advanceTimeBy(3_999) - expectNoEvents() - - advanceTimeBy(1) - assertThat(awaitItem()).isEmpty() - } - } - - @Test - fun `it completes after the last scheduled re-emission when upstream completes`() = runTest { - val shares = listOf(aLiveLocationShare(endTimestamp = 1_000)) - flowOf(shares) - .timedByExpiry(currentTimeMillis = { testScheduler.currentTime }) - .test { - assertThat(awaitItem()).isEqualTo(shares) - - advanceTimeBy(1_000) - assertThat(awaitItem()).isEmpty() - - awaitComplete() - } - } - - @Test - fun `it completes immediately when upstream emits nothing`() = runTest { - emptyFlow>() - .timedByExpiry(currentTimeMillis = { testScheduler.currentTime }) - .test { - awaitComplete() - } - } -} 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 65f2d1e2d5..f298da8b42 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,8 +30,6 @@ class RoomPowerLevelsValuesMapperTest { roomAvatar = 9, roomTopic = 10, spaceChild = 11, - beacon = 12, - beaconInfo = 13, ) ) ).isEqualTo( @@ -46,8 +44,6 @@ 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 d951b3d339..6fac54b904 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,68 +173,16 @@ 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( - analyticsService: FakeAnalyticsService = FakeAnalyticsService(), - ) = RoomSummaryListProcessor( + private fun TestScope.createProcessor() = RoomSummaryListProcessor( summaries, FakeFfiRoomListService(), coroutineContext = StandardTestDispatcher(testScheduler), roomSummaryFactory = RoomSummaryFactory(), - analyticsService = analyticsService, + analyticsService = FakeAnalyticsService(), ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessorTest.kt deleted file mode 100644 index 7a3a091f4e..0000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessorTest.kt +++ /dev/null @@ -1,118 +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.timeline.postprocessor - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem -import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem -import org.junit.Test - -private const val TODAY = 1_779_779_967_000 -private const val YESTERDAY = TODAY - 24 * 60 * 60 * 1000 -private const val DAY_BEFORE_YESTERDAY = YESTERDAY - 24 * 60 * 60 * 1000 - -class FilterEmptyDayPostProcessorTest { - private val anEvent = MatrixTimelineItem.Event( - uniqueId = UniqueId("event"), - event = anEventTimelineItem(), - ) - - private fun aDaySeparator(timestmap: Long) = MatrixTimelineItem.Virtual( - uniqueId = UniqueId("day_$timestmap"), - virtual = VirtualTimelineItem.DayDivider(timestmap) - ) - - @Test - fun `filterEmptyDaySeparators keeps day separator with events after it`() { - val items = listOf( - anEvent, - aDaySeparator(TODAY), - ) - val result = FilterEmptyDayPostProcessor().process(items) - assertThat(result).hasSize(2) - assertThat(result[0]).isEqualTo(anEvent) - assertThat(result[1]).isEqualTo(aDaySeparator(TODAY)) - } - - @Test - fun `filterEmptyDaySeparators removes day separator with no events after it`() { - val items = listOf( - aDaySeparator(TODAY), - aDaySeparator(YESTERDAY), - ) - val result = FilterEmptyDayPostProcessor().process(items) - assertThat(result).isEmpty() - } - - @Test - fun `filterEmptyDaySeparators removes first day separator and keeps second when only second has events`() { - val items = listOf( - aDaySeparator(TODAY), - anEvent, - aDaySeparator(YESTERDAY), - ) - val result = FilterEmptyDayPostProcessor().process(items) - assertThat(result).hasSize(2) - assertThat(result[0]).isEqualTo(anEvent) - assertThat(result[1]).isEqualTo(aDaySeparator(YESTERDAY)) - } - - @Test - fun `filterEmptyDaySeparators handles multiple day separators in a row with no events`() { - val items = listOf( - aDaySeparator(TODAY), - aDaySeparator(YESTERDAY), - aDaySeparator(DAY_BEFORE_YESTERDAY), - ) - val result = FilterEmptyDayPostProcessor().process(items) - assertThat(result).isEmpty() - } - - @Test - fun `filterEmptyDaySeparators keeps all items when no day separators`() { - val items = listOf( - anEvent, - anEvent.copy(uniqueId = UniqueId("event2")), - ) - val result = FilterEmptyDayPostProcessor().process(items) - assertThat(result).hasSize(2) - } - - @Test - fun `filterEmptyDaySeparators removes day separator followed by non-event virtual item`() { - val readMarker = MatrixTimelineItem.Virtual( - uniqueId = UniqueId("readMarker"), - virtual = VirtualTimelineItem.ReadMarker - ) - val items = listOf( - aDaySeparator(TODAY), - readMarker, - ) - val result = FilterEmptyDayPostProcessor().process(items) - assertThat(result).hasSize(1) - assertThat(result[0]).isEqualTo(readMarker) - } - - @Test - fun `filterEmptyDaySeparators keeps day separator when non-event virtual items are between separator and event`() { - val readMarker = MatrixTimelineItem.Virtual( - uniqueId = UniqueId("readMarker"), - virtual = VirtualTimelineItem.ReadMarker - ) - val items = listOf( - anEvent, - readMarker, - aDaySeparator(TODAY), - ) - val result = FilterEmptyDayPostProcessor().process(items) - assertThat(result).hasSize(3) - assertThat(result[2]).isEqualTo(aDaySeparator(TODAY)) - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessorTest.kt deleted file mode 100644 index dd899baf5b..0000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessorTest.kt +++ /dev/null @@ -1,75 +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.timeline.postprocessor - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.room.join.JoinRule -import org.junit.Test - -class FilterPublicMembershipChangesPostProcessorTest { - @Test - fun `processor removes join, leave, and profile events in unencrypted public rooms`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - otherMemberJoinEvent, - messageEvent, - otherMemberLeaveEvent, - profileChangeEvent, - ) - val expected = listOf( - roomCreateEvent, - messageEvent, - ) - val processor = FilterPublicMembershipChangesPostProcessor() - val processedItems = processor.process( - timelineItems, - joinRule = JoinRule.Public, - isEncrypted = false, - ) - assertThat(processedItems).isEqualTo(expected) - } - - @Test - fun `processor keeps all events in encrypted public rooms`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - otherMemberJoinEvent, - messageEvent, - otherMemberLeaveEvent, - profileChangeEvent, - ) - val processor = FilterPublicMembershipChangesPostProcessor() - val processedItems = processor.process( - timelineItems, - joinRule = JoinRule.Public, - isEncrypted = true, - ) - assertThat(processedItems).isEqualTo(timelineItems) - } - - @Test - fun `processor keeps membership events in invite-only rooms`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - otherMemberJoinEvent, - messageEvent, - otherMemberLeaveEvent, - profileChangeEvent, - ) - val processor = FilterPublicMembershipChangesPostProcessor() - val processedItems = processor.process( - timelineItems, - joinRule = JoinRule.Invite, - isEncrypted = null, - ) - assertThat(processedItems).isEqualTo(timelineItems) - } -} 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 cda5a07b8e..50f8637096 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,7 +17,6 @@ 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 @@ -37,14 +36,6 @@ 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 680422827c..dbeba39973 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,12 +96,7 @@ 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) } @@ -112,12 +107,7 @@ 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() } @@ -127,12 +117,7 @@ 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() } @@ -143,12 +128,7 @@ 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/walletsecretstorage/RecoveryKeyDerivationTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/RecoveryKeyDerivationTest.kt new file mode 100644 index 0000000000..4571d7ceb1 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/RecoveryKeyDerivationTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.libraries.matrix.impl.walletsecretstorage + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import kotlin.random.Random + +class RecoveryKeyDerivationTest { + private fun validKey(seed: Int = 1): String { + val entropy = Random(seed).nextBytes(32) + return RecoveryKeyDerivation.encodeRecoveryKeyForTesting(entropy) + } + + @Test + fun `round-trip — encode then derive returns 32 bytes`() { + val key = validKey() + val derived = RecoveryKeyDerivation.deriveKey(key) + assertThat(derived).isNotNull() + assertThat(derived!!.size).isEqualTo(32) + } + + @Test + fun `derivation is deterministic for the same input`() { + val key = validKey() + val a = RecoveryKeyDerivation.deriveKey(key) + val b = RecoveryKeyDerivation.deriveKey(key) + assertThat(a).isEqualTo(b) + } + + @Test + fun `different recovery keys produce different derived keys`() { + val a = RecoveryKeyDerivation.deriveKey(validKey(seed = 1)) + val b = RecoveryKeyDerivation.deriveKey(validKey(seed = 2)) + assertThat(a).isNotEqualTo(b) + } + + @Test + fun `whitespace in recovery key is ignored`() { + val tight = validKey().replace(" ", "") + // Insert spaces every 4 chars, then some tabs/newlines for good measure + val spaced = tight.chunked(4).joinToString(" ") + val weird = tight.replace("A", "A \t\n ") + + val a = RecoveryKeyDerivation.deriveKey(tight) + val b = RecoveryKeyDerivation.deriveKey(spaced) + val c = RecoveryKeyDerivation.deriveKey(weird) + + assertThat(a).isNotNull() + assertThat(a).isEqualTo(b) + assertThat(a).isEqualTo(c) + } + + @Test + fun `distinct info labels produce distinct keys`() { + val key = validKey() + val walletKey = RecoveryKeyDerivation.deriveKey(key, "sulkta.wallet.seed.v1") + val otherKey = RecoveryKeyDerivation.deriveKey(key, "sulkta.something.else.v1") + + assertThat(walletKey).isNotNull() + assertThat(otherKey).isNotNull() + assertThat(walletKey).isNotEqualTo(otherKey) + } + + @Test + fun `default label matches the explicit wallet-seed label`() { + val key = validKey() + val defaultLabelKey = RecoveryKeyDerivation.deriveKey(key) + val explicitLabelKey = RecoveryKeyDerivation.deriveKey(key, RecoveryKeyDerivation.WALLET_SEED_KEY_LABEL) + assertThat(defaultLabelKey).isEqualTo(explicitLabelKey) + } + + @Test + fun `returns null for empty input`() { + assertThat(RecoveryKeyDerivation.deriveKey("")).isNull() + assertThat(RecoveryKeyDerivation.deriveKey(" ")).isNull() + } + + @Test + fun `returns null for non-base58 characters`() { + // '0', 'O', 'I', 'l' are not in the Bitcoin base58 alphabet + assertThat(RecoveryKeyDerivation.deriveKey("0000000000000000")).isNull() + assertThat(RecoveryKeyDerivation.deriveKey("!!!not valid!!!")).isNull() + } + + @Test + fun `returns null for wrong-length decoded payload`() { + // A short valid base58 string decodes to only a few bytes — wrong length. + assertThat(RecoveryKeyDerivation.deriveKey("abc")).isNull() + } + + @Test + fun `flipped parity byte rejects the key`() { + val tight = validKey().replace(" ", "") + // Change the last character to a different valid base58 digit — very + // likely breaks parity. We try several, at least one must be rejected + // (if every substitution coincidentally kept parity valid, that would + // indicate parity isn't being checked at all). + val candidates = listOf("2", "3", "4", "5", "6", "7", "8", "9") + .map { tight.dropLast(1) + it } + .filter { it != tight } + + val rejections = candidates.count { RecoveryKeyDerivation.deriveKey(it) == null } + assertThat(rejections).isGreaterThan(0) + } + + @Test + fun `flipped prefix byte rejects the key`() { + // Build a key with the wrong prefix byte, valid parity. + val entropy = ByteArray(32) { 1 } + val raw = ByteArray(35) + raw[0] = 0x8c.toByte() // wrong — spec says 0x8b + raw[1] = 0x01 + System.arraycopy(entropy, 0, raw, 2, 32) + var parity: Byte = 0 + for (i in 0 until raw.size - 1) parity = parity.xor(raw[i]) + raw[34] = parity + // Encode manually using the exposed helper + // (not available — but we can cheat by flipping a char in a valid key and + // accepting some entropy will reject before we even reach the prefix check). + // A simpler direct test: the valid-key path already exercises the prefix + // check via the parity failure cases above. This test stays as a + // documentation that the prefix check exists; real coverage would + // require a private-method unit test or exposing encode with a custom prefix. + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/WalletSecretEnvelopeTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/WalletSecretEnvelopeTest.kt new file mode 100644 index 0000000000..974a0b0d96 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/walletsecretstorage/WalletSecretEnvelopeTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.libraries.matrix.impl.walletsecretstorage + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import kotlin.random.Random + +class WalletSecretEnvelopeTest { + private val aad = "com.sulkta.wallet.seed.v1" + private fun key(seed: Int = 0): ByteArray = Random(seed).nextBytes(32) + + @Test + fun `round trip returns original plaintext`() { + val k = key() + val plaintext = "wild alley ribbon chunk pear sauce flight glass shallow ivory glue smart".toByteArray() + val sealed = WalletSecretEnvelope.seal(k, aad, plaintext) + + val opened = WalletSecretEnvelope.open(k, aad, sealed) + + assertThat(opened).isEqualTo(plaintext) + } + + @Test + fun `each seal call produces a fresh IV and distinct ciphertext`() { + val k = key() + val pt = "same plaintext".toByteArray() + + val a = WalletSecretEnvelope.seal(k, aad, pt) + val b = WalletSecretEnvelope.seal(k, aad, pt) + + assertThat(a).isNotEqualTo(b) + assertThat(WalletSecretEnvelope.open(k, aad, a)).isEqualTo(pt) + assertThat(WalletSecretEnvelope.open(k, aad, b)).isEqualTo(pt) + } + + @Test + fun `wrong key fails open (returns null)`() { + val correct = key(seed = 1) + val wrong = key(seed = 2) + val sealed = WalletSecretEnvelope.seal(correct, aad, "hello".toByteArray()) + + val opened = WalletSecretEnvelope.open(wrong, aad, sealed) + + assertThat(opened).isNull() + } + + @Test + fun `wrong aad fails open`() { + val k = key() + val sealed = WalletSecretEnvelope.seal(k, aad, "hello".toByteArray()) + + val opened = WalletSecretEnvelope.open(k, "com.different.slot", sealed) + + assertThat(opened).isNull() + } + + @Test + fun `tampered ciphertext fails open`() { + val k = key() + val sealed = WalletSecretEnvelope.seal(k, aad, "hello".toByteArray()) + // Flip a byte in the base64 ciphertext field + val tampered = sealed.replaceFirst("\"ct\":\"", "\"ct\":\"X") + + val opened = WalletSecretEnvelope.open(k, aad, tampered) + + assertThat(opened).isNull() + } + + @Test + fun `tampered iv fails open`() { + val k = key() + val sealed = WalletSecretEnvelope.seal(k, aad, "hello".toByteArray()) + val tampered = sealed.replaceFirst("\"iv\":\"", "\"iv\":\"X") + + val opened = WalletSecretEnvelope.open(k, aad, tampered) + + assertThat(opened).isNull() + } + + @Test + fun `wrong envelope version fails open`() { + val k = key() + val sealed = WalletSecretEnvelope.seal(k, aad, "hello".toByteArray()) + val futureVersion = sealed.replace("\"v\":1", "\"v\":2") + + val opened = WalletSecretEnvelope.open(k, aad, futureVersion) + + assertThat(opened).isNull() + } + + @Test + fun `malformed JSON fails open`() { + val opened = WalletSecretEnvelope.open(key(), aad, "not json") + assertThat(opened).isNull() + } + + @Test + fun `wrong-sized key throws`() { + val tooShort = ByteArray(16) + runCatching { WalletSecretEnvelope.seal(tooShort, aad, "hi".toByteArray()) } + .exceptionOrNull() + .also { assertThat(it).isInstanceOf(IllegalArgumentException::class.java) } + } +} diff --git a/libraries/matrix/test/build.gradle.kts b/libraries/matrix/test/build.gradle.kts index ccfa56f1aa..63836d857a 100644 --- a/libraries/matrix/test/build.gradle.kts +++ b/libraries/matrix/test/build.gradle.kts @@ -19,7 +19,6 @@ dependencies { api(projects.libraries.matrix.api) api(libs.coroutines.core) implementation(libs.coroutines.test) - implementation(projects.libraries.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 af28dc37e4..dfb62e122e 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,7 @@ 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.oauth.AccountManagementAction +import io.element.android.libraries.matrix.api.oidc.AccountManagementAction 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,7 +34,6 @@ 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 @@ -43,7 +42,9 @@ import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.walletsecretstorage.WalletSecretStorage import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.libraries.matrix.test.walletsecretstorage.FakeWalletSecretStorage import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader import io.element.android.libraries.matrix.test.media.FakeMediaPreviewService import io.element.android.libraries.matrix.test.notification.FakeNotificationService @@ -83,6 +84,7 @@ class FakeMatrixClient( override val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), override val syncService: SyncService = FakeSyncService(), override val encryptionService: EncryptionService = FakeEncryptionService(), + override val walletSecretStorage: WalletSecretStorage = FakeWalletSecretStorage(), override val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(), override val mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(), override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), @@ -108,7 +110,6 @@ 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()) }, 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 3b2fdeafb1..238ad2663d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt @@ -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.OAuthDetails -import io.element.android.libraries.matrix.api.auth.OAuthPrompt +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.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 AN_OAUTH_DATA = OAuthDetails(url = "a-url") +val A_OIDC_DATA = OidcDetails(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 oAuthError: Throwable? = null - private var oAuthCancelError: Throwable? = null + private var oidcError: Throwable? = null + private var oidcCancelError: 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 getOAuthUrl( - prompt: OAuthPrompt, + override suspend fun getOidcUrl( + prompt: OidcPrompt, loginHint: String?, - ): Result = simulateLongTask { - oAuthError?.let { Result.failure(it) } ?: Result.success(AN_OAUTH_DATA) + ): Result = simulateLongTask { + oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA) } - override suspend fun cancelOAuthLogin(): Result { - return oAuthCancelError?.let { Result.failure(it) } ?: Result.success(Unit) + override suspend fun cancelOidcLogin(): Result { + return oidcCancelError?.let { Result.failure(it) } ?: Result.success(Unit) } - override suspend fun loginWithOAuth(callbackUrl: String): Result = simulateLongTask { + override suspend fun loginWithOidc(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 givenOAuthError(throwable: Throwable?) { - oAuthError = throwable + fun givenOidcError(throwable: Throwable?) { + oidcError = throwable } - fun givenOAuthCancelError(throwable: Throwable?) { - oAuthCancelError = throwable + fun givenOidcCancelError(throwable: Throwable?) { + oidcCancelError = throwable } fun givenLoginError(throwable: Throwable?) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOAuthRedirectUrlProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOidcRedirectUrlProvider.kt similarity index 75% rename from libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOAuthRedirectUrlProvider.kt rename to libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOidcRedirectUrlProvider.kt index fcb28b48ba..47c9b0951d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOAuthRedirectUrlProvider.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOidcRedirectUrlProvider.kt @@ -8,12 +8,12 @@ package io.element.android.libraries.matrix.test.auth -import io.element.android.libraries.matrix.api.auth.OAuthRedirectUrlProvider +import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider const val FAKE_REDIRECT_URL = "io.element.android:/" -class FakeOAuthRedirectUrlProvider( +class FakeOidcRedirectUrlProvider( private val provideResult: String = FAKE_REDIRECT_URL, -) : OAuthRedirectUrlProvider { +) : OidcRedirectUrlProvider { 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 56fd4de4c3..3b9573bcfc 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, - supportsOAuthLogin: Boolean = false, + supportsOidcLogin: Boolean = false, ) = MatrixHomeServerDetails( url = url, supportsPasswordLogin = supportsPasswordLogin, - supportsOAuthLogin = supportsOAuthLogin, + supportsOidcLogin = supportsOidcLogin, ) 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 d65328d7fe..06ffeb547c 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.IdentityOAuthResetHandle +import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle -class FakeIdentityOAuthResetHandle( +class FakeIdentityOidcResetHandle( override val url: String = "", - var resetOAuthLambda: () -> Result = { error("Not implemented") }, + var resetOidcLambda: () -> Result = { error("Not implemented") }, var cancelLambda: () -> Unit = { error("Not implemented") }, -) : IdentityOAuthResetHandle { - override suspend fun resetOAuth(): Result { - return resetOAuthLambda() +) : IdentityOidcResetHandle { + override suspend fun resetOidc(): Result { + return resetOidcLambda() } 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 f7970e7971..564cd231b2 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, isDM: Boolean): Result { + override suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result { val error = setDefaultNotificationModeError if (error != null) { return Result.failure(error) } - if (isDM) { + if (isOneToOne) { 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 5ea2de888b..4ceddc414c 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 @@ -223,6 +223,4 @@ 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 b4425ddd4b..acdad8225d 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,7 +34,6 @@ 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 @@ -89,7 +88,7 @@ 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() }, ) : JoinedRoom, BaseRoom by baseRoom { @@ -134,11 +133,7 @@ class FakeJoinedRoom( } override suspend fun updateRoomNotificationSettings(): Result = simulateLongTask { - val notificationSettings = roomNotificationSettingsService.getRoomNotificationSettings( - roomId = roomId, - isEncrypted = info().isEncrypted.orFalse(), - isOneToOne = isDm(), - ).getOrThrow() + val notificationSettings = roomNotificationSettingsService.getRoomNotificationSettings(roomId, info().isEncrypted.orFalse(), isOneToOne).getOrThrow() (roomNotificationSettingsStateFlow as MutableStateFlow).value = RoomNotificationSettingsState.Ready(notificationSettings) return Result.success(Unit) } @@ -243,8 +238,8 @@ class FakeJoinedRoom( return liveLocationSharesFlow } - override suspend fun startLiveLocationShare(durationMillis: Long): Result = simulateLongTask { - startLiveLocationShareResult(durationMillis).map { AN_EVENT_ID } + override suspend fun startLiveLocationShare(durationMillis: Long): Result = simulateLongTask { + startLiveLocationShareResult(durationMillis) } override suspend fun stopLiveLocationShare(): Result = simulateLongTask { @@ -255,6 +250,11 @@ class FakeJoinedRoom( sendLiveLocationResult(geoUri) } + var sendRawEventResult: (String, String) -> Result = { _, _ -> Result.success(Unit) } + override suspend fun sendRawEvent(eventType: String, content: String): Result = simulateLongTask { + sendRawEventResult(eventType, content) + } + 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 3293570eec..c7faaba627 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,8 +71,6 @@ fun aRoomInfo( privilegedCreatorRole: Boolean = false, isLowPriority: Boolean = false, activeCallIntentConsensus: CallIntentConsensus = CallIntentConsensus.None, - isDm: Boolean = false, - fullyReadEventId: EventId? = null, ) = RoomInfo( id = id, name = name, @@ -111,6 +109,4 @@ 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 33a1b68ba3..f6bc0c5ec2 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,7 +23,6 @@ fun aRoomMember( isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, - isServiceMember: Boolean = false, ) = RoomMember( userId = userId, displayName = displayName, @@ -34,7 +33,6 @@ 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 392aa880c9..afe4c88f5a 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,7 +81,6 @@ fun aRoomSummary( privilegedCreatorRole: Boolean = false, isLowPriority: Boolean = false, activeCallIntentConsensus: CallIntentConsensus = CallIntentConsensus.None, - fullyReadEventId: EventId? = null, ) = RoomSummary( info = RoomInfo( id = roomId, @@ -121,8 +120,6 @@ 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 deleted file mode 100644 index 23730c8886..0000000000 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/location/LiveLocationFixture.kt +++ /dev/null @@ -1,38 +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.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/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index fcc7057dbe..302013044e 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -61,6 +61,20 @@ class FakeTimeline( lambdaError() } ) : Timeline { + var sendRawLambda: ( + eventType: String, + content: String, + ) -> Result = { _, _ -> + Result.success(Unit) + } + + override suspend fun sendRaw( + eventType: String, + content: String, + ): Result = simulateLongTask { + sendRawLambda(eventType, content) + } + var sendMessageLambda: ( body: String, htmlBody: String?, 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 0aeeebf86d..f4f12a435e 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 requestDeviceVerificationLambda: () -> Unit = { lambdaError() }, + private val requestCurrentSessionVerificationLambda: () -> 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 startSasVerificationLambda: () -> Unit = { lambdaError() }, + private val startVerificationLambda: () -> 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 requestDeviceVerification() = simulateLongTask { - requestDeviceVerificationLambda() + override suspend fun requestCurrentSessionVerification() { + requestCurrentSessionVerificationLambda() } - override suspend fun requestUserVerification(userId: UserId) = simulateLongTask { + override suspend fun requestUserVerification(userId: UserId) { requestUserVerificationLambda(userId) } - override suspend fun cancelVerification() = simulateLongTask { + override suspend fun cancelVerification() { cancelVerificationLambda() } - override suspend fun approveVerification() = simulateLongTask { + override suspend fun approveVerification() { approveVerificationLambda() } - override suspend fun declineVerification() = simulateLongTask { + override suspend fun declineVerification() { declineVerificationLambda() } - override suspend fun startSasVerification() = simulateLongTask { - startSasVerificationLambda() + override suspend fun startVerification() { + startVerificationLambda() } - override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) = simulateLongTask { + override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) { resetLambda(cancelAnyPendingVerificationAttempt) } @@ -75,7 +75,7 @@ class FakeSessionVerificationService( this.listener = listener } - override suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) = simulateLongTask { + override suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) { acknowledgeVerificationRequestLambda(verificationRequest) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/walletsecretstorage/FakeWalletSecretStorage.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/walletsecretstorage/FakeWalletSecretStorage.kt new file mode 100644 index 0000000000..841ace9ea3 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/walletsecretstorage/FakeWalletSecretStorage.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Sulkta Coop. + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package io.element.android.libraries.matrix.test.walletsecretstorage + +import io.element.android.libraries.matrix.api.walletsecretstorage.WalletSecretStorage + +/** + * In-memory fake for [WalletSecretStorage]. Stores the last put value as + * plaintext keyed on the recovery key so tests can round-trip without + * standing up real crypto / real account data. + */ +class FakeWalletSecretStorage : WalletSecretStorage { + private val store: MutableMap = mutableMapOf() + var putSeedResult: (String, String) -> Result = { recoveryKey, seed -> + store[recoveryKey] = seed + Result.success(Unit) + } + var getSeedResult: (String) -> Result = { recoveryKey -> + Result.success(store[recoveryKey]) + } + var hasSeedBackupResult: () -> Result = { Result.success(store.isNotEmpty()) } + var deleteSeedResult: () -> Result = { + store.clear() + Result.success(Unit) + } + + override suspend fun putSeed(recoveryKey: String, seedPhrase: String): Result = + putSeedResult(recoveryKey, seedPhrase) + + override suspend fun getSeed(recoveryKey: String): Result = + getSeedResult(recoveryKey) + + override suspend fun hasSeedBackup(): Result = hasSeedBackupResult() + + override suspend fun deleteSeed(): Result = deleteSeedResult() +} diff --git a/libraries/matrixmedia/impl/build.gradle.kts b/libraries/matrixmedia/impl/build.gradle.kts index 56ccc79afc..82afc2f62c 100644 --- a/libraries/matrixmedia/impl/build.gradle.kts +++ b/libraries/matrixmedia/impl/build.gradle.kts @@ -19,10 +19,8 @@ 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 ef654a4cf7..e67e630e97 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,6 +16,7 @@ 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 @@ -28,7 +29,7 @@ interface ImageLoaderFactory { @ContributesBinding(AppScope::class) class DefaultImageLoaderFactory( @ApplicationContext private val context: Context, - private val okHttpClient: () -> OkHttpClient, + private val okHttpClient: Provider, ) : ImageLoaderFactory { private val okHttpNetworkFetcherFactory = OkHttpNetworkFetcherFactory( callFactory = { 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..4628833bb4 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 @@ -67,7 +67,6 @@ fun AvatarActionBottomSheet( }, modifier = modifier, sheetState = sheetState, - scrollable = false, ) { AvatarActionBottomSheetContent( actions = actions, 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 4d3ba4dc6b..e743a2447c 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,7 +42,6 @@ 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 @@ -197,10 +196,7 @@ 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) - .clearAndSetSemantics { - hideFromAccessibility() - }, + .border(BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary), shape = CircleShape), contentAlignment = Alignment.Center, ) { Icon( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt index d663177ee6..7e0e51050a 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt @@ -14,8 +14,6 @@ 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.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -56,17 +54,18 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun CreateDmConfirmationBottomSheet( matrixUser: MatrixUser, + enableKeyShareOnInvite: Boolean, isUserIdentityUnknown: Boolean, onSendInvite: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - val titleContent = if (isUserIdentityUnknown) { + val titleContent = if (enableKeyShareOnInvite && isUserIdentityUnknown) { stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_title) } else { stringResource(R.string.screen_bottom_sheet_create_dm_title) } - val descriptionContent = if (isUserIdentityUnknown) { + val descriptionContent = if (enableKeyShareOnInvite && isUserIdentityUnknown) { stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_content) } else { stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName()) @@ -76,13 +75,11 @@ fun CreateDmConfirmationBottomSheet( modifier = modifier, onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), - scrollable = false, ) { Column( modifier = Modifier .fillMaxWidth() - .padding(top = 24.dp, bottom = 16.dp, start = 16.dp, end = 16.dp) - .verticalScroll(rememberScrollState()), + .padding(top = 24.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { if (isUserIdentityUnknown) { @@ -152,13 +149,12 @@ fun CreateDmConfirmationBottomSheet( @PreviewsDayNight @Composable -internal fun CreateDmConfirmationBottomSheetPreview( - @PreviewParameter( - CreateDmConfirmationBottomSheetStateProvider::class - ) state: CreateDmConfirmationBottomSheetState -) = ElementPreview { +internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter( + CreateDmConfirmationBottomSheetStateProvider::class +) state: CreateDmConfirmationBottomSheetState) = ElementPreview { CreateDmConfirmationBottomSheet( matrixUser = state.matrixUser, + enableKeyShareOnInvite = state.enableKeyShareOnInvite, isUserIdentityUnknown = state.isUserIdentityUnknown, onSendInvite = {}, onDismiss = {}, @@ -167,12 +163,14 @@ internal fun CreateDmConfirmationBottomSheetPreview( data class CreateDmConfirmationBottomSheetState( val matrixUser: MatrixUser, + val enableKeyShareOnInvite: Boolean, val isUserIdentityUnknown: Boolean, ) class CreateDmConfirmationBottomSheetStateProvider : PreviewParameterProvider { override val values = sequenceOf( - CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), isUserIdentityUnknown = false), - CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), isUserIdentityUnknown = true), - ) + CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = false, isUserIdentityUnknown = false), + CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = true, isUserIdentityUnknown = false), + CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = true, isUserIdentityUnknown = true), + ) } diff --git a/libraries/matrixui/src/main/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 12fa54add1..13747a1a3d 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,7 +22,6 @@ 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 @@ -58,10 +57,10 @@ internal fun InviteSenderViewPreview() = ElementPreview { InviteSenderView( inviteSender = InviteSender( userId = UserId("@bob:example.com"), - displayName = USER_NAME_BOB, + displayName = "Bob", avatarData = AvatarData( id = "@bob:example.com", - name = USER_NAME_BOB, + name = "Bob", url = null, size = AvatarSize.InviteSender, ), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt index 0b9bde0fb9..5b44b50ce9 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt @@ -8,7 +8,6 @@ package io.element.android.libraries.matrix.ui.components -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -35,34 +34,51 @@ import io.element.android.libraries.matrix.ui.model.getBestName @Composable fun MatrixUserHeader( + matrixUser: MatrixUser?, + modifier: Modifier = Modifier, + // TODO handle click on this item, to let the user be able to update their profile. + // onClick: () -> Unit, +) { + if (matrixUser == null) { + MatrixUserHeaderPlaceholder(modifier = modifier) + } else { + MatrixUserHeaderContent( + matrixUser = matrixUser, + modifier = modifier, + // onClick = onClick + ) + } +} + +@Composable +private fun MatrixUserHeaderContent( matrixUser: MatrixUser, modifier: Modifier = Modifier, + // onClick: () -> Unit, ) { Row( modifier = modifier + // .clickable(onClick = onClick) .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically ) { Avatar( modifier = Modifier - .padding(vertical = 7.dp), + .padding(vertical = 12.dp), avatarData = matrixUser.getAvatarData(size = AvatarSize.UserPreference), avatarType = AvatarType.User, ) - Spacer(modifier = Modifier.width(13.dp)) + Spacer(modifier = Modifier.width(16.dp)) Column( - modifier = Modifier - .weight(1f) - .padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) + modifier = Modifier.weight(1f) ) { // Name Text( modifier = Modifier.clipToBounds(), text = matrixUser.getBestName(), maxLines = 1, - style = ElementTheme.typography.fontHeadingMdRegular, + style = ElementTheme.typography.fontHeadingSmMedium, overflow = TextOverflow.Ellipsis, color = ElementTheme.colors.textPrimary, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeaderPlaceholder.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeaderPlaceholder.kt new file mode 100644 index 0000000000..08f8a37cc1 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeaderPlaceholder.kt @@ -0,0 +1,64 @@ +/* + * 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.ui.components + +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom +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.theme.placeholderBackground + +@Composable +fun MatrixUserHeaderPlaceholder( + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .padding(vertical = 12.dp) + .size(AvatarSize.UserPreference.dp) + .background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape) + ) + Spacer(modifier = Modifier.width(16.dp)) + Column( + modifier = Modifier.weight(1f) + ) { + PlaceholderAtom(width = 80.dp, height = 7.dp) + Spacer(modifier = Modifier.height(16.dp)) + PlaceholderAtom(width = 180.dp, height = 6.dp) + } + } +} + +@PreviewsDayNight +@Composable +internal fun MatrixUserHeaderPlaceholderPreview() = ElementPreview { + MatrixUserHeaderPlaceholder() +} 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 639bafbcbf..4d5a1cd222 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,17 +9,6 @@ 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 @@ -31,33 +20,42 @@ open class MatrixUserProvider : PreviewParameterProvider { ) } +open class MatrixUserWithNullProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aMatrixUser(), + aMatrixUser(displayName = null), + null, + ) +} + open class MatrixUserWithAvatarProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aMatrixUser(displayName = USER_NAME_JOHN_DOE), - aMatrixUser(displayName = USER_NAME_JOHN_DOE, avatarUrl = "anUrl"), + aMatrixUser(displayName = "John Doe"), + aMatrixUser(displayName = "John Doe", avatarUrl = "anUrl"), ) } fun aMatrixUser( - id: String? = null, - displayName: String? = USER_NAME_ALICE, + id: String = "@id_of_alice:server.org", + displayName: String? = "Alice", avatarUrl: String? = null, ) = MatrixUser( - userId = UserId(id ?: "@${displayName?.lowercase()?.replace(" ", "_") ?: "id"}:server.org"), + userId = UserId(id), displayName = displayName, avatarUrl = avatarUrl, ) fun aMatrixUserList() = listOf( - 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), + 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"), ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt index ed7fd63435..cf89074737 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt @@ -11,8 +11,6 @@ package io.element.android.libraries.matrix.ui.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp 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 @@ -25,14 +23,12 @@ fun MatrixUserRow( matrixUser: MatrixUser, modifier: Modifier = Modifier, avatarSize: AvatarSize = AvatarSize.UserListItem, - verticalSpaceWidth: Dp = 12.dp, trailingContent: @Composable (() -> Unit)? = null, ) = UserRow( avatarData = matrixUser.getAvatarData(avatarSize), name = matrixUser.getBestName(), subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value, modifier = modifier, - verticalSpaceWidth = verticalSpaceWidth, trailingContent = trailingContent, ) 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 0a1e3bdc87..374ebd7a1a 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,7 +27,6 @@ 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 @@ -73,7 +72,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 17ec7c6ec7..b118dbcbe3 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,7 +18,6 @@ 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 @@ -59,7 +58,7 @@ internal fun SelectedUserRtlPreview() = CompositionLocalProvider( ) { ElementPreview { SelectedUser( - matrixUser = aMatrixUser(displayName = USER_NAME_JOHN_DOE), + matrixUser = aMatrixUser(displayName = "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 7aee62902f..c3e8292147 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,11 +30,6 @@ 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 @@ -120,15 +115,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 = 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), + 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"), ), 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 6d927989d5..202ea79d87 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,10 +22,6 @@ 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 @@ -102,10 +98,10 @@ internal fun SpaceMembersViewPreview() = ElementPreview( ) { SpaceMembersView( heroes = persistentListOf( - 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), + 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"), ), numberOfMembers = 123, ) 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 d6a582d83c..db63bba779 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,8 +9,6 @@ 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 @@ -30,10 +28,10 @@ class SpaceRoomProvider : PreviewParameterProvider { state = CurrentUserMembership.LEFT, ), aSpaceRoom( - displayName = SPACE_NAME, + displayName = "Alice", roomType = RoomType.Room, isDirect = true, - heroes = listOf(aMatrixUser(displayName = USER_NAME_ALICE)), + heroes = listOf(aMatrixUser(displayName = "Alice")), state = CurrentUserMembership.JOINED, numJoinedMembers = 2, ), @@ -71,9 +69,9 @@ class SpaceRoomProvider : PreviewParameterProvider { state = CurrentUserMembership.INVITED, ), aSpaceRoom( - displayName = SPACE_NAME, + displayName = "Alice", roomType = RoomType.Space, - heroes = listOf(aMatrixUser(displayName = USER_NAME_ALICE)), + heroes = listOf(aMatrixUser(displayName = "Alice")), state = CurrentUserMembership.JOINED, numJoinedMembers = 2, ), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt index 8d236d1a2a..9bcf0b323f 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt @@ -10,16 +10,13 @@ package io.element.android.libraries.matrix.ui.components 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.padding -import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.text.style.TextOverflow -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.components.avatar.Avatar @@ -34,22 +31,22 @@ internal fun UserRow( subtext: String?, modifier: Modifier = Modifier, enabled: Boolean = true, - verticalSpaceWidth: Dp = 12.dp, trailingContent: @Composable (() -> Unit)? = null, ) { Row( modifier = modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp), verticalAlignment = Alignment.CenterVertically ) { Avatar( avatarData = avatarData, avatarType = AvatarType.User, ) - Spacer(modifier = Modifier.width(verticalSpaceWidth)) Column( - modifier = Modifier.weight(1f), + modifier = Modifier + .padding(start = 12.dp) + .weight(1f), ) { // Name Text( 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 d29a28cba9..ee9de51681 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,10 +12,8 @@ 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 org.jsoup.Jsoup +import io.element.android.wysiwyg.utils.HtmlToDomParser 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. @@ -36,9 +34,9 @@ fun FormattedBody.toHtmlDocument( ?.trimEnd() ?.let { formattedBody -> val dom = if (prefix != null) { - CustomHtmlToDomParser.document("$prefix $formattedBody") + HtmlToDomParser.document("$prefix $formattedBody") } else { - CustomHtmlToDomParser.document(formattedBody) + HtmlToDomParser.document(formattedBody) } // Prepend `@` to mentions @@ -62,35 +60,3 @@ 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 cf8b03b80d..d58d12b785 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,7 +11,6 @@ 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 @@ -27,19 +26,6 @@ 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`. @@ -65,8 +51,6 @@ fun Document.toPlainText(): String { return visitor.build() } -private const val FALLBACK_REPLY_NODE_TAG = "mx-reply" - private class PlainTextNodeVisitor : NodeVisitor { private val builder = StringBuilder() @@ -94,9 +78,6 @@ 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 ca118349e9..5ddf57b723 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,7 +9,6 @@ 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 @@ -160,7 +159,7 @@ private fun aInReplyToDetails( ) fun aProfileDetailsReady( - displayName: String? = USER_NAME_SENDER, + displayName: String? = "Sender", displayNameAmbiguous: Boolean = false, avatarUrl: String? = null, ) = ProfileDetails.Ready( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt index 9e5e468cd9..773590a8c1 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadata.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.res.stringResource import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType @@ -131,5 +132,6 @@ internal fun InReplyToDetails.Ready.metadata(hideImage: Boolean): InReplyToMetad is LegacyCallInviteContent, is CallNotifyContent, is LiveLocationContent, + is CustomEventContent, null -> null } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt index 4d0e02aa6f..0dc8aac09e 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt @@ -56,7 +56,6 @@ fun InReplyToView( inReplyTo: InReplyToDetails, hideImage: Boolean, modifier: Modifier = Modifier, - maxLines: Int = 2, ) { when (inReplyTo) { is InReplyToDetails.Ready -> { @@ -64,12 +63,11 @@ fun InReplyToView( senderId = inReplyTo.senderId, senderProfile = inReplyTo.senderProfile, metadata = inReplyTo.metadata(hideImage), - maxLines = maxLines, modifier = modifier, ) } is InReplyToDetails.Error -> - ReplyToErrorContent(data = inReplyTo, maxLines = maxLines, modifier = modifier) + ReplyToErrorContent(data = inReplyTo, modifier = modifier) is InReplyToDetails.Loading -> ReplyToLoadingContent(modifier = modifier) } @@ -80,7 +78,6 @@ private fun ReplyToReadyContent( senderId: UserId, senderProfile: ProfileDetails, metadata: InReplyToMetadata?, - maxLines: Int, modifier: Modifier = Modifier, ) { val paddings = if (metadata is InReplyToMetadata.Thumbnail) { @@ -118,7 +115,7 @@ private fun ReplyToReadyContent( traversalIndex = 1f }, ) - ReplyToContentText(metadata, maxLines) + ReplyToContentText(metadata) } } } @@ -143,7 +140,6 @@ private fun ReplyToLoadingContent( @Composable private fun ReplyToErrorContent( data: InReplyToDetails.Error, - maxLines: Int, modifier: Modifier = Modifier, ) { val paddings = PaddingValues(horizontal = 12.dp, vertical = 4.dp) @@ -156,17 +152,14 @@ private fun ReplyToErrorContent( text = data.message, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textCriticalPrimary, - maxLines = maxLines, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) } } @Composable -private fun ReplyToContentText( - metadata: InReplyToMetadata?, - maxLines: Int, -) { +private fun ReplyToContentText(metadata: InReplyToMetadata?) { val text = when (metadata) { InReplyToMetadata.Redacted -> stringResource(id = CommonStrings.common_message_removed) InReplyToMetadata.UnableToDecrypt -> stringResource(id = CommonStrings.common_waiting_for_decryption_key) @@ -207,7 +200,7 @@ private fun ReplyToContentText( fontStyle = fontStyle, textAlign = TextAlign.Start, color = ElementTheme.colors.textSecondary, - maxLines = maxLines, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) } diff --git a/libraries/matrixui/src/main/res/values-ca/translations.xml b/libraries/matrixui/src/main/res/values-ca/translations.xml deleted file mode 100644 index 44d3e10c55..0000000000 --- a/libraries/matrixui/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - "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-cs/translations.xml b/libraries/matrixui/src/main/res/values-cs/translations.xml index cdfb34c032..a8e82b288c 100644 --- a/libraries/matrixui/src/main/res/values-cs/translations.xml +++ b/libraries/matrixui/src/main/res/values-cs/translations.xml @@ -3,7 +3,5 @@ "Poslat pozvánku" "Chcete začít chatovat s %1$s?" "Poslat pozvánku?" - "Momentálně s touto osobou nemáte žádné chaty. Před pokračováním potvrďte pozvání." - "Chcete zahájit chat s tímto novým kontaktem?" "%1$s (%2$s) vás pozval(a)" diff --git a/libraries/matrixui/src/main/res/values-da/translations.xml b/libraries/matrixui/src/main/res/values-da/translations.xml index f2dfa27073..5b4970f003 100644 --- a/libraries/matrixui/src/main/res/values-da/translations.xml +++ b/libraries/matrixui/src/main/res/values-da/translations.xml @@ -3,7 +3,5 @@ "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 a84990af10..bd9840940e 100644 --- a/libraries/matrixui/src/main/res/values-et/translations.xml +++ b/libraries/matrixui/src/main/res/values-et/translations.xml @@ -3,7 +3,5 @@ "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-fi/translations.xml b/libraries/matrixui/src/main/res/values-fi/translations.xml index b1b971eadb..daba3555bb 100644 --- a/libraries/matrixui/src/main/res/values-fi/translations.xml +++ b/libraries/matrixui/src/main/res/values-fi/translations.xml @@ -3,7 +3,5 @@ "Lähetä kutsu" "Haluaisitko aloittaa keskustelun käyttäjän %1$s kanssa?" "Lähetetäänkö kutsu?" - "Sinulla ei ole tällä hetkellä keskusteluja tämän henkilön kanssa. Vahvista kutsu ennen jatkamista." - "Aloitetaanko keskustelu tämän uuden kontaktin kanssa?" "%1$s (%2$s) kutsui sinut" diff --git a/libraries/matrixui/src/main/res/values-fr/translations.xml b/libraries/matrixui/src/main/res/values-fr/translations.xml index 7e6466d264..ca952f53c0 100644 --- a/libraries/matrixui/src/main/res/values-fr/translations.xml +++ b/libraries/matrixui/src/main/res/values-fr/translations.xml @@ -3,7 +3,5 @@ "Envoyer l’invitation" "Voulez-vous entamer une discussion avec %1$s ?" "Envoyer l’invitation ?" - "Vous n’avez actuellement aucune conversation avec cette personne. Confirmez son invitation avant de continuer." - "Entamer une conversation avec ce nouveau contact ?" "%1$s (%2$s) vous a invité(e)" diff --git a/libraries/matrixui/src/main/res/values-hr/translations.xml b/libraries/matrixui/src/main/res/values-hr/translations.xml index 6cd6f9fe7f..333d8fd7b7 100644 --- a/libraries/matrixui/src/main/res/values-hr/translations.xml +++ b/libraries/matrixui/src/main/res/values-hr/translations.xml @@ -3,7 +3,5 @@ "Pošalji pozivnicu" "Želite li započeti razgovor s korisnikom %1$s?" "Želite li poslati pozivnicu?" - "Trenutno nemate razgovora s ovom osobom. Potvrdite pozivanje prije nego što nastavite." - "Želite li započeti razgovor s ovim novim kontaktom?" "Pozvao vas je korisnik %1$s (%2$s)" diff --git a/libraries/matrixui/src/main/res/values-hu/translations.xml b/libraries/matrixui/src/main/res/values-hu/translations.xml index 6bd7999bff..f22454cd16 100644 --- a/libraries/matrixui/src/main/res/values-hu/translations.xml +++ b/libraries/matrixui/src/main/res/values-hu/translations.xml @@ -3,7 +3,5 @@ "Meghívó küldése" "Csevegést kezd vele: %1$s?" "Meghívó küldése?" - "Még nem beszélgetett ezzel a személlyel. Folytatás előtt erősítse meg a meghívást." - "Csevegést kezdeményez ezzel az új felhasználóval?" "%1$s (%2$s) meghívta" diff --git a/libraries/matrixui/src/main/res/values-it/translations.xml b/libraries/matrixui/src/main/res/values-it/translations.xml index 913c45867c..439d61337e 100644 --- a/libraries/matrixui/src/main/res/values-it/translations.xml +++ b/libraries/matrixui/src/main/res/values-it/translations.xml @@ -3,7 +3,5 @@ "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-ja/translations.xml b/libraries/matrixui/src/main/res/values-ja/translations.xml index 8834c0c18c..ce2b24041c 100644 --- a/libraries/matrixui/src/main/res/values-ja/translations.xml +++ b/libraries/matrixui/src/main/res/values-ja/translations.xml @@ -3,7 +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 bd31d7edb4..caa2c0e07d 100644 --- a/libraries/matrixui/src/main/res/values-pl/translations.xml +++ b/libraries/matrixui/src/main/res/values-pl/translations.xml @@ -3,7 +3,5 @@ "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 4d3eaa2364..5156d6bd16 100644 --- a/libraries/matrixui/src/main/res/values-ro/translations.xml +++ b/libraries/matrixui/src/main/res/values-ro/translations.xml @@ -3,7 +3,5 @@ "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..ef6b724c1c 100644 --- a/libraries/matrixui/src/main/res/values-ru/translations.xml +++ b/libraries/matrixui/src/main/res/values-ru/translations.xml @@ -3,7 +3,5 @@ "Отправить приглашение" "Хотите начать чат с %1$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 4d2063fbd2..324bb96086 100644 --- a/libraries/matrixui/src/main/res/values-uk/translations.xml +++ b/libraries/matrixui/src/main/res/values-uk/translations.xml @@ -3,7 +3,5 @@ "Надіслати запрошення" "Хочете розпочати бесіду з %1$s?" "Надіслати запрошення?" - "Наразі у вас немає чатів із цим користувачем. Підтвердьте запрошення, перш ніж продовжити." - "Розпочати чат із цим новим контактом?" "%1$s (%2$s) запрошує вас" diff --git a/libraries/matrixui/src/main/res/values-uz/translations.xml b/libraries/matrixui/src/main/res/values-uz/translations.xml index 4510619c7a..63add2d3c0 100644 --- a/libraries/matrixui/src/main/res/values-uz/translations.xml +++ b/libraries/matrixui/src/main/res/values-uz/translations.xml @@ -3,7 +3,5 @@ "Taklif yuborish" "%1$s bilan chatni boshlashni xohlaysizmi?" "Taklif yuborilsinmi?" - "Ayni paytda bu shaxs bilan hech qanday suhbatingiz yo‘q. Davom etishdan oldin ularni taklif qilishni tasdiqlang." - "Bu yangi kontakt bilan chat boshlansinmi?" "%1$s(%2$s ) sizni taklif qildi" diff --git a/libraries/matrixui/src/main/res/values-zh-rTW/translations.xml b/libraries/matrixui/src/main/res/values-zh-rTW/translations.xml index 1d216abfea..f851e399fe 100644 --- a/libraries/matrixui/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/matrixui/src/main/res/values-zh-rTW/translations.xml @@ -3,7 +3,5 @@ "傳送邀請" "您想要開始與 %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 e05e21095d..aa8479fea0 100644 --- a/libraries/matrixui/src/main/res/values-zh/translations.xml +++ b/libraries/matrixui/src/main/res/values-zh/translations.xml @@ -1,9 +1,7 @@ "发送邀请" - "你是否要与 %1$s 开始聊天?" + "您想与%1$s 开始聊天吗?" "发送邀请?" - "你与此人暂无任何聊天。请确认对方被邀请后再继续。" - "是否与新联系人开始聊天?" - "%1$s(%2$s)邀请了你" + "%1$s (%2$s)邀请了你" 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 ce0ef4cf31..607f825401 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 @@ -136,19 +136,4 @@ 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/room/RoomMembersTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/RoomMembersTest.kt index 737fff34ee..816ac0967e 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,10 +30,13 @@ class RoomMembersTest { private val roomMember3 = aRoomMember(A_USER_ID_3) @Test - fun `getDirectRoomMember emits other member for encrypted DM`() = runTest { + fun `getDirectRoomMember emits other member for encrypted DM with 2 joined members`() = runTest { val joinedRoom = FakeBaseRoom( sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo(isDm = true, isEncrypted = true) + initialRoomInfo = aRoomInfo( + isDirect = true, + joinedMembersCount = 2, + ) ) moleculeFlow(RecompositionMode.Immediate) { joinedRoom.getDirectRoomMember( @@ -48,7 +51,7 @@ class RoomMembersTest { fun `getDirectRoomMember emit null if the room is not a dm`() = runTest { val joinedRoom = FakeBaseRoom( sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo(isDm = false) + initialRoomInfo = aRoomInfo(isDirect = false) ) moleculeFlow(RecompositionMode.Immediate) { joinedRoom.getDirectRoomMember( @@ -63,7 +66,10 @@ class RoomMembersTest { fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest { val joinedRoom = FakeBaseRoom( sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo(isDm = true) + initialRoomInfo = aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) ) moleculeFlow(RecompositionMode.Immediate) { joinedRoom.getDirectRoomMember( @@ -74,11 +80,42 @@ 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(isDm = true), + initialRoomInfo = aRoomInfo(isDirect = true), ) moleculeFlow(RecompositionMode.Immediate) { joinedRoom.getDirectRoomMember( @@ -98,7 +135,10 @@ class RoomMembersTest { fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest { val joinedRoom = FakeBaseRoom( sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo(isDm = true) + initialRoomInfo = aRoomInfo( + isDirect = true, + activeMembersCount = 2, + ) ) moleculeFlow(RecompositionMode.Immediate) { joinedRoom.getDirectRoomMember( diff --git a/libraries/mediaupload/test/build.gradle.kts b/libraries/mediaupload/test/build.gradle.kts index f271ae1316..7e729089c7 100644 --- a/libraries/mediaupload/test/build.gradle.kts +++ b/libraries/mediaupload/test/build.gradle.kts @@ -15,7 +15,6 @@ 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 3750060eef..c07ebb6ec9 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,10 +31,6 @@ 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"), @@ -55,7 +51,6 @@ 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 fedf376a60..74b479dc8c 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,7 +17,6 @@ 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, @@ -34,7 +33,6 @@ 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, @@ -42,7 +40,6 @@ fun anImageMediaInfo( filename = "an image file.jpg", fileSize = 4 * 1024 * 1024, caption = caption, - formattedCaption = formattedCaption, mimeType = MimeTypes.Jpeg, formattedFileSize = "4MB", fileExtension = "jpg", @@ -57,7 +54,6 @@ fun anImageMediaInfo( fun aVideoMediaInfo( caption: String? = null, - formattedCaption: CharSequence? = null, senderName: String? = null, dateSent: String? = null, dateSentFull: String? = null, @@ -66,7 +62,6 @@ fun aVideoMediaInfo( filename = "a video file.mp4", fileSize = 14 * 1024 * 1024, caption = caption, - formattedCaption = formattedCaption, mimeType = MimeTypes.Mp4, formattedFileSize = "14MB", fileExtension = "mp4", @@ -82,7 +77,6 @@ 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, @@ -90,7 +84,6 @@ fun aPdfMediaInfo( filename = filename, fileSize = 23 * 1024 * 1024, caption = caption, - formattedCaption = formattedCaption, mimeType = MimeTypes.Pdf, formattedFileSize = "23MB", fileExtension = "pdf", @@ -112,7 +105,6 @@ fun anApkMediaInfo( filename = "an apk file.apk", fileSize = 50 * 1024 * 1024, caption = null, - formattedCaption = null, mimeType = MimeTypes.Apk, formattedFileSize = "50MB", fileExtension = "apk", @@ -128,7 +120,6 @@ 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, @@ -138,7 +129,6 @@ fun anAudioMediaInfo( filename = filename, fileSize = 7 * 1024 * 1024, caption = caption, - formattedCaption = formattedCaption, mimeType = MimeTypes.Mp3, formattedFileSize = "7MB", fileExtension = "mp3", @@ -154,7 +144,6 @@ 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, @@ -164,7 +153,6 @@ fun aVoiceMediaInfo( filename = filename, fileSize = 3 * 1024 * 1024, caption = caption, - formattedCaption = formattedCaption, mimeType = MimeTypes.Ogg, formattedFileSize = "3MB", fileExtension = "ogg", @@ -180,7 +168,6 @@ 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, @@ -188,7 +175,6 @@ fun aTxtMediaInfo( filename = filename, fileSize = 2 * 1024, caption = caption, - formattedCaption = formattedCaption, mimeType = MimeTypes.PlainText, formattedFileSize = "2kB", fileExtension = "txt", diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts index b723578829..9c2342ecd9 100644 --- a/libraries/mediaviewer/impl/build.gradle.kts +++ b/libraries/mediaviewer/impl/build.gradle.kts @@ -25,13 +25,9 @@ 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) @@ -50,7 +46,6 @@ 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 138f6382d2..e1e112ecfa 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,7 +32,6 @@ 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 898a29aa7a..02e3b15fa0 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 @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessag import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent +import io.element.android.libraries.matrix.api.timeline.item.event.CustomEventContent import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl @@ -65,7 +66,7 @@ class EventItemFactory( mode = DateFormatterMode.Full, ) return when (val content = event.content) { - is CallNotifyContent, + CallNotifyContent, is FailedToParseMessageLikeContent, is FailedToParseStateContent, LegacyCallInviteContent, @@ -77,7 +78,8 @@ class EventItemFactory( is StickerContent, is UnableToDecryptContent, is LiveLocationContent, - UnknownContent -> { + UnknownContent, + is CustomEventContent -> { Timber.w("Should not happen: ${content.javaClass.simpleName}") null } @@ -98,7 +100,6 @@ 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), @@ -119,7 +120,6 @@ 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), @@ -141,7 +141,6 @@ 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), @@ -163,7 +162,6 @@ 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), @@ -185,7 +183,6 @@ 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), @@ -207,7 +204,6 @@ 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 7bdd4872a3..722e14a790 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,7 +17,6 @@ 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 @@ -28,12 +27,10 @@ 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 { - val isReady: Boolean - fun start(coroutineScope: CoroutineScope) + fun start() fun groupedMediaItemsFlow(): Flow> fun getLastData(): AsyncData suspend fun loadMore(direction: Timeline.PaginationDirection) @@ -50,7 +47,7 @@ class TimelineMediaGalleryDataSource( ) : MediaGalleryDataSource { private var timeline: Timeline? = null - private val groupedMediaItemsFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 10) + private val groupedMediaItemsFlow = MutableSharedFlow>(replay = 1) override fun groupedMediaItemsFlow(): Flow> = groupedMediaItemsFlow @@ -60,12 +57,9 @@ class TimelineMediaGalleryDataSource( private val isStarted = AtomicBoolean(false) - override val isReady: Boolean get() = isStarted.get() && timeline != null - @OptIn(ExperimentalCoroutinesApi::class) - override fun start(coroutineScope: CoroutineScope) { + override fun start() { if (!isStarted.compareAndSet(false, true)) { - Timber.w("MediaGalleryDataSource for room ${room.roomId} is already started, ignoring subsequent start call") return } flow { @@ -77,12 +71,10 @@ 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)) }, ) @@ -104,22 +96,19 @@ class TimelineMediaGalleryDataSource( groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems)) } .onCompletion { - timeline?.let { - Timber.d("Timeline media gallery data source flow completed for room ${room.roomId}, closing timeline") - it.close() - } + timeline?.close() } - .launchIn(coroutineScope) + .launchIn(room.roomCoroutineScope) } override suspend fun loadMore(direction: Timeline.PaginationDirection) { - timeline?.paginate(direction) ?: Timber.w("Timeline is not ready yet, cannot load more media items for room ${room.roomId}") + timeline?.paginate(direction) } 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..7cd4dee318 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 @@ -15,16 +15,16 @@ import io.element.android.libraries.mediaviewer.api.MediaInfo sealed interface MediaBottomSheetState { data object Hidden : MediaBottomSheetState - data class Details( + data class MediaDeleteConfirmationState( + val eventId: EventId, + val mediaInfo: MediaInfo, + val thumbnailSource: MediaSource?, + ) : MediaBottomSheetState + + data class MediaDetailsBottomSheetState( val eventId: EventId?, val canDelete: Boolean, val mediaInfo: MediaInfo, val thumbnailSource: MediaSource?, ) : MediaBottomSheetState - - data class DeleteConfirmation( - val eventId: EventId, - val mediaInfo: MediaInfo, - val thumbnailSource: MediaSource?, - ) : MediaBottomSheetState } 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 deleted file mode 100644 index 72b16c4e1d..0000000000 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationProvider.kt +++ /dev/null @@ -1,37 +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.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 -import io.element.android.libraries.mediaviewer.api.anImageMediaInfo - -open class MediaBottomSheetStateDeleteConfirmationProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aMediaBottomSheetStateDeleteConfirmation(), - aMediaBottomSheetStateDeleteConfirmation( - thumbnailSource = MediaSource("url_thumbnail") - ), - ) -} - -fun aMediaBottomSheetStateDeleteConfirmation( - mediaInfo: MediaInfo = anImageMediaInfo( - senderName = USER_NAME_ALICE, - ), - thumbnailSource: MediaSource? = null, -) = MediaBottomSheetState.DeleteConfirmation( - eventId = EventId("\$eventId"), - mediaInfo = mediaInfo, - thumbnailSource = thumbnailSource, -) 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 deleted file mode 100644 index 99a6925b9d..0000000000 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsProvider.kt +++ /dev/null @@ -1,47 +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.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 -import io.element.android.libraries.mediaviewer.api.anImageMediaInfo - -open class MediaBottomSheetStateDetailsProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aMediaBottomSheetStateDetails(), - aMediaBottomSheetStateDetails( - canDelete = false, - ), - aMediaBottomSheetStateDetails( - mediaInfo = anApkMediaInfo( - dateSentFull = "December 6, 2024 at 12:59", - ), - ), - aMediaBottomSheetStateDetails( - eventId = null, - ), - ) -} - -fun aMediaBottomSheetStateDetails( - eventId: EventId? = EventId($$"$eventId"), - canDelete: Boolean = true, - mediaInfo: MediaInfo = anImageMediaInfo( - senderName = USER_NAME_ALICE, - dateSentFull = "December 6, 2024 at 12:59", - ), -) = MediaBottomSheetState.Details( - eventId = eventId, - canDelete = canDelete, - mediaInfo = mediaInfo, - thumbnailSource = null, -) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt index 0cae7f0262..3850f3a0f8 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt @@ -17,11 +17,8 @@ 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.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -30,7 +27,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import io.element.android.compound.theme.ElementTheme @@ -46,12 +42,11 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.mediaviewer.impl.R import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.libraries.ui.strings.Strings @OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaDeleteConfirmationBottomSheet( - state: MediaBottomSheetState.DeleteConfirmation, + state: MediaBottomSheetState.MediaDeleteConfirmationState, onDelete: (EventId) -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, @@ -59,13 +54,10 @@ fun MediaDeleteConfirmationBottomSheet( ModalBottomSheet( modifier = modifier, onDismissRequest = onDismiss, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), - scrollable = false, ) { Column( modifier = Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp), ) { IconTitleSubtitleMolecule( @@ -108,7 +100,7 @@ fun MediaDeleteConfirmationBottomSheet( @Composable private fun MediaRow( - state: MediaBottomSheetState.DeleteConfirmation, + state: MediaBottomSheetState.MediaDeleteConfirmationState, modifier: Modifier = Modifier, ) { Row( @@ -151,7 +143,7 @@ private fun MediaRow( ) // Info Text( - text = state.mediaInfo.mimeType + Strings.NICE_SEPARATOR + state.mediaInfo.formattedFileSize, + text = state.mediaInfo.mimeType + " - " + state.mediaInfo.formattedFileSize, color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -163,11 +155,9 @@ private fun MediaRow( @PreviewsDayNight @Composable -internal fun MediaDeleteConfirmationBottomSheetPreview( - @PreviewParameter(provider = MediaBottomSheetStateDeleteConfirmationProvider::class) state: MediaBottomSheetState.DeleteConfirmation, -) = ElementPreview { +internal fun MediaDeleteConfirmationBottomSheetPreview() = ElementPreview { MediaDeleteConfirmationBottomSheet( - state = state, + state = aMediaDeleteConfirmationState(), onDelete = {}, onDismiss = {}, ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt index 0c73bfd17c..a6c30796af 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt @@ -10,14 +10,11 @@ package io.element.android.libraries.mediaviewer.impl.details import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row 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.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,15 +22,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.heading -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -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.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -51,20 +43,15 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.impl.R import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.libraries.ui.strings.Strings -/** - * Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=2229-149220 - */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaDetailsBottomSheet( - state: MediaBottomSheetState.Details, + state: MediaBottomSheetState.MediaDetailsBottomSheetState, onViewInTimeline: (EventId) -> Unit, onShare: (EventId) -> Unit, onForward: (EventId) -> Unit, onDownload: (EventId) -> Unit, - onOpenWith: (EventId) -> Unit, onDelete: (EventId) -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, @@ -72,14 +59,13 @@ fun MediaDetailsBottomSheet( ModalBottomSheet( modifier = modifier, onDismissRequest = onDismiss, - scrollable = false, ) { Column( modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp), ) { - Title() Section( title = stringResource(R.string.screen_media_details_uploaded_by), ) { @@ -97,75 +83,57 @@ fun MediaDetailsBottomSheet( ) SectionText( title = stringResource(R.string.screen_media_details_file_format), - text = state.mediaInfo.mimeType + Strings.NICE_SEPARATOR + state.mediaInfo.formattedFileSize, + text = state.mediaInfo.mimeType + " - " + state.mediaInfo.formattedFileSize, ) - Spacer(modifier = Modifier.height(16.dp)) if (state.eventId != null) { - HorizontalDivider() - ListItem( - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VisibilityOn())), - headlineContent = { Text(stringResource(CommonStrings.action_view_in_timeline)) }, - style = ListItemStyle.Primary, - onClick = { - onViewInTimeline(state.eventId) - } - ) - ListItem( - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ShareAndroid())), - headlineContent = { Text(stringResource(CommonStrings.action_share)) }, - style = ListItemStyle.Primary, - onClick = { - onShare(state.eventId) - } - ) - ListItem( - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Forward())), - headlineContent = { Text(stringResource(CommonStrings.action_forward)) }, - style = ListItemStyle.Primary, - onClick = { - onForward(state.eventId) - } - ) - ListItem( - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Download())), - headlineContent = { Text(stringResource(CommonStrings.action_download)) }, - style = ListItemStyle.Primary, - onClick = { - onDownload(state.eventId) - } - ) - val mimeType = state.mediaInfo.mimeType - val icon = when (mimeType) { - MimeTypes.Apk -> - ListItemContent.Icon(IconSource.Resource(R.drawable.ic_apk_install)) - else -> - ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())) - } - val wording = when (mimeType) { - MimeTypes.Apk -> stringResource(id = CommonStrings.common_install_apk_android) - else -> stringResource(id = CommonStrings.action_open_with) - } - ListItem( - leadingContent = icon, - headlineContent = { Text(wording) }, - style = ListItemStyle.Primary, - onClick = { - onOpenWith(state.eventId) - } - ) - if (state.canDelete) { + Column { HorizontalDivider() ListItem( - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Delete())), - headlineContent = { Text(stringResource(CommonStrings.action_delete)) }, - style = ListItemStyle.Destructive, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VisibilityOn())), + headlineContent = { Text(stringResource(CommonStrings.action_view_in_timeline)) }, + style = ListItemStyle.Primary, onClick = { - onDelete(state.eventId) + onViewInTimeline(state.eventId) } ) + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ShareAndroid())), + headlineContent = { Text(stringResource(CommonStrings.action_share)) }, + style = ListItemStyle.Primary, + onClick = { + onShare(state.eventId) + } + ) + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Forward())), + headlineContent = { Text(stringResource(CommonStrings.action_forward)) }, + style = ListItemStyle.Primary, + onClick = { + onForward(state.eventId) + } + ) + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Download())), + headlineContent = { Text(stringResource(CommonStrings.action_save)) }, + style = ListItemStyle.Primary, + onClick = { + onDownload(state.eventId) + } + ) + if (state.canDelete) { + HorizontalDivider() + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Delete())), + headlineContent = { Text(stringResource(CommonStrings.action_remove)) }, + style = ListItemStyle.Destructive, + onClick = { + onDelete(state.eventId) + } + ) + } + Spacer(modifier = Modifier.height(16.dp)) } } - Spacer(modifier = Modifier.height(16.dp)) } } } @@ -196,46 +164,27 @@ private fun SenderRow( .weight(1f), ) { // Name - val bestName = mediaInfo.senderName ?: mediaInfo.senderId?.value.orEmpty() val avatarColors = AvatarColorsProvider.provide(id) Text( modifier = Modifier.clipToBounds(), - text = bestName, + text = mediaInfo.senderName.orEmpty(), maxLines = 1, overflow = TextOverflow.Ellipsis, color = avatarColors.foreground, style = ElementTheme.typography.fontBodyMdMedium, ) // Id - if (!mediaInfo.senderName.isNullOrEmpty()) { - Text( - text = mediaInfo.senderId?.value.orEmpty(), - color = ElementTheme.colors.textSecondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ElementTheme.typography.fontBodyMdRegular, - ) - } + Text( + text = mediaInfo.senderId?.value.orEmpty(), + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = ElementTheme.typography.fontBodyMdRegular, + ) } } } -@Composable -private fun ColumnScope.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, - ) -} - @Composable private fun Section( title: String, @@ -244,12 +193,12 @@ private fun Section( Column( modifier = Modifier .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp), + .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { Text( - text = title, - style = ElementTheme.typography.fontBodyMdMedium, + text = title.uppercase(), + style = ElementTheme.typography.fontBodySmRegular, color = ElementTheme.colors.textSecondary, ) content() @@ -272,16 +221,13 @@ private fun SectionText( @PreviewsDayNight @Composable -internal fun MediaDetailsBottomSheetPreview( - @PreviewParameter(MediaBottomSheetStateDetailsProvider::class) state: MediaBottomSheetState.Details, -) = ElementPreview { +internal fun MediaDetailsBottomSheetPreview() = ElementPreview { MediaDetailsBottomSheet( - state = state, + state = aMediaDetailsBottomSheetState(), onViewInTimeline = {}, onShare = {}, onForward = {}, onDownload = {}, - onOpenWith = {}, onDelete = {}, onDismiss = {}, ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/Preview.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/Preview.kt new file mode 100644 index 0000000000..a152a32091 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/Preview.kt @@ -0,0 +1,37 @@ +/* + * 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.mediaviewer.impl.details + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.mediaviewer.api.anImageMediaInfo + +fun aMediaDetailsBottomSheetState( + dateSentFull: String = "December 6, 2024 at 12:59", + canDelete: Boolean = true, +): MediaBottomSheetState.MediaDetailsBottomSheetState { + return MediaBottomSheetState.MediaDetailsBottomSheetState( + eventId = EventId("\$eventId"), + canDelete = canDelete, + mediaInfo = anImageMediaInfo( + senderName = "Alice", + dateSentFull = dateSentFull, + ), + thumbnailSource = null, + ) +} + +fun aMediaDeleteConfirmationState(): MediaBottomSheetState.MediaDeleteConfirmationState { + return MediaBottomSheetState.MediaDeleteConfirmationState( + eventId = EventId("\$eventId"), + mediaInfo = anImageMediaInfo( + senderName = "Alice", + ), + thumbnailSource = null, + ) +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvent.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt similarity index 75% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvent.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt index 219d5dbfb4..2bf4f6b37d 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvent.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt @@ -14,22 +14,21 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.impl.model.MediaItem -sealed interface MediaGalleryEvent { - data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvent - data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvent - data class Share(val eventId: EventId) : MediaGalleryEvent - data class Forward(val eventId: EventId) : MediaGalleryEvent - data class SaveOnDisk(val eventId: EventId) : MediaGalleryEvent - data class OpenWith(val eventId: EventId) : MediaGalleryEvent - data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvent - data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvent +sealed interface MediaGalleryEvents { + data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents + data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents + data class Share(val eventId: EventId) : MediaGalleryEvents + data class Forward(val eventId: EventId) : MediaGalleryEvents + data class SaveOnDisk(val eventId: EventId) : MediaGalleryEvents + data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents + data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents data class ConfirmDelete( val eventId: EventId, val mediaInfo: MediaInfo, val thumbnailSource: MediaSource?, - ) : MediaGalleryEvent + ) : MediaGalleryEvents - data object CloseBottomSheet : MediaGalleryEvent - data class Delete(val eventId: EventId) : MediaGalleryEvent + data object CloseBottomSheet : MediaGalleryEvents + data class Delete(val eventId: EventId) : MediaGalleryEvents } 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 2c2fb31020..cc26e69c33 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(this) + mediaGalleryDataSource.start() } val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms -> @@ -88,47 +88,39 @@ class MediaGalleryPresenter( val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() localMediaActions.Configure() - fun handleEvent(event: MediaGalleryEvent) { + fun handleEvent(event: MediaGalleryEvents) { when (event) { - is MediaGalleryEvent.ChangeMode -> { + is MediaGalleryEvents.ChangeMode -> { mode = event.mode } - is MediaGalleryEvent.LoadMore -> coroutineScope.launch { - if (mediaGalleryDataSource.isReady) { - mediaGalleryDataSource.loadMore(event.direction) - } + is MediaGalleryEvents.LoadMore -> coroutineScope.launch { + mediaGalleryDataSource.loadMore(event.direction) } - is MediaGalleryEvent.Delete -> coroutineScope.launch { + is MediaGalleryEvents.Delete -> coroutineScope.launch { mediaGalleryDataSource.deleteItem(event.eventId) } - is MediaGalleryEvent.SaveOnDisk -> coroutineScope.launch { + is MediaGalleryEvents.SaveOnDisk -> coroutineScope.launch { mediaBottomSheetState = MediaBottomSheetState.Hidden groupedMediaItems.dataOrNull().find(event.eventId)?.let { saveOnDisk(it) } } - is MediaGalleryEvent.OpenWith -> coroutineScope.launch { - mediaBottomSheetState = MediaBottomSheetState.Hidden - groupedMediaItems.dataOrNull().find(event.eventId)?.let { - openWith(it) - } - } - is MediaGalleryEvent.Share -> coroutineScope.launch { + is MediaGalleryEvents.Share -> coroutineScope.launch { mediaBottomSheetState = MediaBottomSheetState.Hidden groupedMediaItems.dataOrNull().find(event.eventId)?.let { share(it) } } - is MediaGalleryEvent.Forward -> { + is MediaGalleryEvents.Forward -> { mediaBottomSheetState = MediaBottomSheetState.Hidden navigator.onForwardClick(event.eventId) } - is MediaGalleryEvent.ViewInTimeline -> { + is MediaGalleryEvents.ViewInTimeline -> { mediaBottomSheetState = MediaBottomSheetState.Hidden navigator.onViewInTimelineClick(event.eventId) } - is MediaGalleryEvent.OpenInfo -> coroutineScope.launch { - mediaBottomSheetState = MediaBottomSheetState.Details( + is MediaGalleryEvents.OpenInfo -> coroutineScope.launch { + mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState( eventId = event.mediaItem.eventId(), canDelete = when (event.mediaItem.mediaInfo().senderId) { null -> false @@ -145,14 +137,14 @@ class MediaGalleryPresenter( }, ) } - is MediaGalleryEvent.ConfirmDelete -> { - mediaBottomSheetState = MediaBottomSheetState.DeleteConfirmation( + is MediaGalleryEvents.ConfirmDelete -> { + mediaBottomSheetState = MediaBottomSheetState.MediaDeleteConfirmationState( eventId = event.eventId, mediaInfo = event.mediaInfo, thumbnailSource = event.thumbnailSource, ) } - MediaGalleryEvent.CloseBottomSheet -> { + MediaGalleryEvents.CloseBottomSheet -> { mediaBottomSheetState = MediaBottomSheetState.Hidden } } @@ -208,17 +200,6 @@ class MediaGalleryPresenter( } } - private suspend fun openWith(mediaItem: MediaItem.Event) { - downloadMedia(mediaItem) - .mapCatchingExceptions { localMedia -> - localMediaActions.open(localMedia) - } - .onFailure { - val snackbarMessage = SnackbarMessage(mediaActionsError(it)) - snackbarDispatcher.post(snackbarMessage) - } - } - private fun mediaActionsError(throwable: Throwable): Int { return if (throwable is ActivityNotFoundException) { R.string.error_no_compatible_app_found diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt index 5dcb487632..897e5d1e97 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt @@ -20,7 +20,7 @@ data class MediaGalleryState( val groupedMediaItems: AsyncData, val mediaBottomSheetState: MediaBottomSheetState, val snackbarMessage: SnackbarMessage?, - val eventSink: (MediaGalleryEvent) -> Unit, + val eventSink: (MediaGalleryEvents) -> Unit, ) enum class MediaGalleryMode(val stringResource: Int) { 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 75b849cd0c..a19f810e45 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,10 +11,9 @@ 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 +import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.aMediaItemAudio @@ -80,7 +79,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/gallery/MediaGalleryView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt index 04da4e8f96..6f7a201fdc 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt @@ -132,7 +132,7 @@ fun MediaGalleryView( index = mode.ordinal, count = MediaGalleryMode.entries.size, selected = state.mode == mode, - onClick = { state.eventSink(MediaGalleryEvent.ChangeMode(mode)) }, + onClick = { state.eventSink(MediaGalleryEvents.ChangeMode(mode)) }, text = stringResource(mode.stringResource), ) } @@ -158,27 +158,24 @@ fun MediaGalleryView( } when (val bottomSheetState = state.mediaBottomSheetState) { MediaBottomSheetState.Hidden -> Unit - is MediaBottomSheetState.Details -> { + is MediaBottomSheetState.MediaDetailsBottomSheetState -> { MediaDetailsBottomSheet( state = bottomSheetState, onViewInTimeline = { eventId -> - state.eventSink(MediaGalleryEvent.ViewInTimeline(eventId)) + state.eventSink(MediaGalleryEvents.ViewInTimeline(eventId)) }, onShare = { eventId -> - state.eventSink(MediaGalleryEvent.Share(eventId)) + state.eventSink(MediaGalleryEvents.Share(eventId)) }, onForward = { eventId -> - state.eventSink(MediaGalleryEvent.Forward(eventId)) + state.eventSink(MediaGalleryEvents.Forward(eventId)) }, onDownload = { eventId -> - state.eventSink(MediaGalleryEvent.SaveOnDisk(eventId)) - }, - onOpenWith = { eventId -> - state.eventSink(MediaGalleryEvent.OpenWith(eventId)) + state.eventSink(MediaGalleryEvents.SaveOnDisk(eventId)) }, onDelete = { eventId -> state.eventSink( - MediaGalleryEvent.ConfirmDelete( + MediaGalleryEvents.ConfirmDelete( eventId = eventId, mediaInfo = bottomSheetState.mediaInfo, thumbnailSource = bottomSheetState.thumbnailSource, @@ -186,18 +183,18 @@ fun MediaGalleryView( ) }, onDismiss = { - state.eventSink(MediaGalleryEvent.CloseBottomSheet) + state.eventSink(MediaGalleryEvents.CloseBottomSheet) }, ) } - is MediaBottomSheetState.DeleteConfirmation -> { + is MediaBottomSheetState.MediaDeleteConfirmationState -> { MediaDeleteConfirmationBottomSheet( state = bottomSheetState, onDelete = { - state.eventSink(MediaGalleryEvent.Delete(it)) + state.eventSink(MediaGalleryEvents.Delete(it)) }, onDismiss = { - state.eventSink(MediaGalleryEvent.CloseBottomSheet) + state.eventSink(MediaGalleryEvents.CloseBottomSheet) }, ) } @@ -216,7 +213,7 @@ private fun MediaGalleryPage( val loadingItem = groupedMediaItems.dataOrNull()?.getItems(mode)?.singleOrNull() as? MediaItem.LoadingIndicator if (loadingItem != null) { LaunchedEffect(loadingItem.timestamp) { - state.eventSink(MediaGalleryEvent.LoadMore(loadingItem.direction)) + state.eventSink(MediaGalleryEvents.LoadMore(loadingItem.direction)) } } LoadingContent(mode) @@ -261,7 +258,7 @@ private fun AsyncData.isLoadingItems(mode: MediaGalleryMode): @Composable private fun MediaGalleryImages( imagesAndVideos: ImmutableList, - eventSink: (MediaGalleryEvent) -> Unit, + eventSink: (MediaGalleryEvents) -> Unit, onItemClick: (MediaItem.Event) -> Unit, ) { if (imagesAndVideos.isEmpty()) { @@ -282,7 +279,7 @@ private fun MediaGalleryImages( @Composable private fun MediaGalleryFiles( files: ImmutableList, - eventSink: (MediaGalleryEvent) -> Unit, + eventSink: (MediaGalleryEvents) -> Unit, onItemClick: (MediaItem.Event) -> Unit, ) { if (files.isEmpty()) { @@ -303,7 +300,7 @@ private fun MediaGalleryFiles( @Composable private fun MediaGalleryFilesList( files: ImmutableList, - eventSink: (MediaGalleryEvent) -> Unit, + eventSink: (MediaGalleryEvents) -> Unit, onItemClick: (MediaItem.Event) -> Unit, ) { val presenterFactories = LocalMediaItemPresenterFactories.current @@ -321,7 +318,7 @@ private fun MediaGalleryFilesList( file = item, onClick = { onItemClick(item) }, onLongClick = { - eventSink(MediaGalleryEvent.OpenInfo(item)) + eventSink(MediaGalleryEvents.OpenInfo(item)) }, ) is MediaItem.Audio -> AudioItemView( @@ -329,7 +326,7 @@ private fun MediaGalleryFilesList( audio = item, onClick = { onItemClick(item) }, onLongClick = { - eventSink(MediaGalleryEvent.OpenInfo(item)) + eventSink(MediaGalleryEvents.OpenInfo(item)) }, ) is MediaItem.Voice -> { @@ -339,7 +336,7 @@ private fun MediaGalleryFilesList( state = presenter.present(), voice = item, onLongClick = { - eventSink(MediaGalleryEvent.OpenInfo(item)) + eventSink(MediaGalleryEvents.OpenInfo(item)) }, ) } @@ -364,7 +361,7 @@ private fun MediaGalleryFilesList( @Composable private fun MediaGalleryImageGrid( imagesAndVideos: ImmutableList, - eventSink: (MediaGalleryEvent) -> Unit, + eventSink: (MediaGalleryEvents) -> Unit, onItemClick: (MediaItem.Event) -> Unit, ) { LazyVerticalGrid( @@ -406,7 +403,7 @@ private fun MediaGalleryImageGrid( image = item, onClick = { onItemClick(item) }, onLongClick = { - eventSink(MediaGalleryEvent.OpenInfo(item)) + eventSink(MediaGalleryEvents.OpenInfo(item)) }, ) is MediaItem.Video -> VideoItemView( @@ -414,7 +411,7 @@ private fun MediaGalleryImageGrid( video = item, onClick = { onItemClick(item) }, onLongClick = { - eventSink(MediaGalleryEvent.OpenInfo(item)) + eventSink(MediaGalleryEvents.OpenInfo(item)) }, ) is MediaItem.LoadingIndicator -> LoadingMoreIndicator( @@ -430,7 +427,7 @@ private fun MediaGalleryImageGrid( @Composable private fun LoadingMoreIndicator( item: MediaItem.LoadingIndicator, - eventSink: (MediaGalleryEvent) -> Unit, + eventSink: (MediaGalleryEvents) -> Unit, modifier: Modifier = Modifier ) { Box( @@ -455,7 +452,7 @@ private fun LoadingMoreIndicator( } val latestEventSink by rememberUpdatedState(eventSink) LaunchedEffect(item.timestamp) { - latestEventSink(MediaGalleryEvent.LoadMore(item.direction)) + latestEventSink(MediaGalleryEvents.LoadMore(item.direction)) } } } 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 09164e36d1..05cbe40f36 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,9 +9,7 @@ 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 @@ -19,7 +17,6 @@ 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 @@ -88,12 +85,8 @@ 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) @@ -104,7 +97,6 @@ class AndroidLocalMediaFactory( filename = fileName, fileSize = fileSize, caption = caption, - formattedCaption = null, formattedFileSize = calculatedFormattedFileSize, fileExtension = fileExtension, senderId = senderId, @@ -117,36 +109,4 @@ 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/audio/MediaAudioView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt index 3ac578196d..cb9d6ae9c8 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt @@ -88,7 +88,7 @@ fun MediaAudioView( modifier: Modifier = Modifier, isDisplayed: Boolean = true, ) { - val exoPlayer = rememberExoPlayer(forAudioOnly = true) + val exoPlayer = rememberExoPlayer() ExoPlayerMediaAudioView( isDisplayed = isDisplayed, localMediaViewState = localMediaViewState, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaMetadata.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaMetadata.kt index dc0090298c..4c267a54dc 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaMetadata.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaMetadata.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.mediaviewer.impl.local.audio import androidx.media3.common.MediaMetadata -import io.element.android.libraries.ui.strings.Strings fun MediaMetadata?.hasArtwork(): Boolean { return this?.artworkData != null || this?.artworkUri != null @@ -23,13 +22,13 @@ fun MediaMetadata?.buildInfo(): String { } if (title != null) { if (isNotEmpty()) { - append(Strings.NICE_SEPARATOR) + append(" - ") } append(title) } if (recordingYear != null) { if (isNotEmpty()) { - append(Strings.NICE_SEPARATOR) + append(" - ") } append(recordingYear) } 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 ccd628dbc5..d0043c657a 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,18 +8,14 @@ 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(forAudioOnly: Boolean): ExoPlayer { +fun rememberExoPlayer(): ExoPlayer { return if (LocalInspectionMode.current) { remember { ExoPlayerForPreview() @@ -27,14 +23,7 @@ fun rememberExoPlayer(forAudioOnly: Boolean): ExoPlayer { } else { val context = LocalContext.current remember { - 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() - } + ExoPlayer.Builder(context).build() } } } 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 1f2b0b93d2..b83c598c10 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,12 +12,13 @@ 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.material3.IconButtonDefaults +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -27,10 +28,8 @@ 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 @@ -92,65 +91,49 @@ fun MediaPlayerControllerView( .widthIn(max = 480.dp), verticalAlignment = Alignment.CenterVertically, ) { - val colors = if (state.isPlaying) { - IconButtonDefaults.iconButtonColors( - containerColor = ElementTheme.colors.bgCanvasDefault, - contentColor = ElementTheme.colors.iconPrimary, - ) + val bgColor = if (state.isPlaying) { + ElementTheme.colors.bgCanvasDefault } else { - IconButtonDefaults.iconButtonColors( - containerColor = ElementTheme.colors.iconPrimary, - contentColor = ElementTheme.colors.iconOnSolidPrimary, - ) + ElementTheme.colors.textPrimary } - val a11yPause = stringResource(CommonStrings.a11y_pause) - val a11yPlay = stringResource(CommonStrings.a11y_play) - IconButton( + Box( modifier = Modifier .size(36.dp) - .semantics { - stateDescription = if (state.isPlaying) a11yPause else a11yPlay - }, - onClick = onTogglePlay, - colors = colors, + .background( + color = bgColor, + shape = CircleShape, + ) + .clip(CircleShape) + .clickable { onTogglePlay() } + .padding(8.dp), + contentAlignment = Alignment.Center, ) { if (state.isPlaying) { Icon( imageVector = CompoundIcons.PauseSolid(), - contentDescription = null, + tint = ElementTheme.colors.iconPrimary, + contentDescription = stringResource(CommonStrings.a11y_pause) ) } else { Icon( imageVector = CompoundIcons.PlaySolid(), - contentDescription = null, + tint = ElementTheme.colors.iconOnSolidPrimary, + contentDescription = stringResource(CommonStrings.a11y_play) ) } } - val position = state.displayProgressInMillis.toHumanReadableDuration() - val a11yPosition = stringResource(CommonStrings.a11y_position, position) Text( modifier = Modifier .widthIn(min = 48.dp) - .padding(horizontal = 8.dp) - .semantics { - contentDescription = a11yPosition - }, - text = position, + .padding(horizontal = 8.dp), + text = state.displayProgressInMillis.toHumanReadableDuration(), textAlign = TextAlign.Center, color = ElementTheme.colors.textPrimary, style = ElementTheme.typography.fontBodyXsMedium, ) var lastSelectedValue by remember { mutableFloatStateOf(-1f) } Slider( - modifier = Modifier - .weight(1f) - .semantics { - // Speak out a progress percent instead of milliseconds - stateDescription = buildString { - append((state.progressAsFloat * 100).toInt()) - append("%") - } - }, + modifier = Modifier.weight(1f), valueRange = 0f..state.durationInMillis.toFloat(), value = lastSelectedValue.takeIf { it >= 0 } ?: state.seekingToMillis?.toFloat() @@ -167,40 +150,30 @@ 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) - .semantics { - contentDescription = a11yDuration - }, + .padding(horizontal = 8.dp), 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 = null, + contentDescription = stringResource(CommonStrings.common_unmute) ) } else { Icon( imageVector = CompoundIcons.VolumeOnSolid(), tint = ElementTheme.colors.iconPrimary, - contentDescription = null, + contentDescription = stringResource(CommonStrings.common_mute) ) } } 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 d89762b72e..082dc0571c 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 @@ -57,7 +57,6 @@ 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 @@ -74,7 +73,7 @@ fun MediaVideoView( audioFocus: AudioFocus?, modifier: Modifier = Modifier, ) { - val exoPlayer = rememberExoPlayer(forAudioOnly = false) + val exoPlayer = rememberExoPlayer() ExoPlayerMediaVideoView( isDisplayed = isDisplayed, localMediaViewState = localMediaViewState, @@ -163,20 +162,12 @@ private fun ExoPlayerMediaVideoView( var autoHideController by remember { mutableIntStateOf(0) } - val isTalkbackActive = isTalkbackActive() - LaunchedEffect(autoHideController, isTalkbackActive) { - if (isTalkbackActive) { - // Ensure that the controller is always visible when talkback is active + LaunchedEffect(autoHideController) { + delay(5.seconds) + if (exoPlayer.isPlaying) { mediaPlayerControllerState = mediaPlayerControllerState.copy( - isVisible = true, + isVisible = false, ) - } 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 b0b81bbde0..928e5d9ca8 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,16 +11,14 @@ 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.mutableStateOf -import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember 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 @@ -37,18 +35,13 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow 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, @@ -57,7 +50,7 @@ class MediaViewerDataSource( private val pagerKeysHandler: PagerKeysHandler, ) { // List of media files that are currently being loaded - private val mediaFiles: ConcurrentHashMap = ConcurrentHashMap() + private val mediaFiles: MutableList = mutableListOf() private val galleryMode = when (mode) { MediaViewerMode.SingleMedia, @@ -69,69 +62,50 @@ class MediaViewerDataSource( private val localMediaStates: MutableMap>> = mutableMapOf() - fun setup(coroutineScope: CoroutineScope) { - galleryDataSource.start(coroutineScope) + fun setup() { + galleryDataSource.start() } fun dispose() { - Timber.d("Disposing MediaViewerDataSource, closing ${mediaFiles.size} media files") - mediaFiles.values.forEach { it.close() } + mediaFiles.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 produceState( - producer: suspend ProduceStateScope>.(StateFlow>) -> Unit - ): State> { - return produceState(initialValue = initialData()) { - producer(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 } + fun collectAsState(): State> { + return remember { dataFlow() }.collectAsState(initialData()) } @VisibleForTesting - 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, + 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, + ) ) - ) - } - 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 = @@ -170,7 +144,7 @@ class MediaViewerDataSource( is MediaItem.LoadingIndicator -> add( MediaViewerPageData.Loading( direction = mediaItem.direction, - timestamp = mediaItem.timestamp, + timestamp = systemClock.epochMillis(), pagerKey = pagerKeysHandler.getKey(mediaItem), ) ) @@ -183,18 +157,10 @@ class MediaViewerDataSource( } suspend fun loadMore(direction: Timeline.PaginationDirection) { - if (galleryDataSource.isReady) { - galleryDataSource.loadMore(direction) - } + 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) @@ -207,7 +173,7 @@ class MediaViewerDataSource( filename = data.mediaInfo.filename ) .onSuccess { mediaFile -> - mediaFiles[data.mediaSource] = mediaFile + mediaFiles.add(mediaFile) } .mapCatchingExceptions { mediaFile -> localMediaFactory.createFromMediaFile( @@ -222,12 +188,4 @@ 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/MediaViewerEvents.kt similarity index 63% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt index 53d287075d..3f1436b9b6 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/MediaViewerEvents.kt @@ -11,23 +11,22 @@ package io.element.android.libraries.mediaviewer.impl.viewer import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline -sealed interface MediaViewerEvent { - data class LoadMedia(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent - data class SaveOnDisk(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent - data class Share(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent - data class OpenWith(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent - data class ClearLoadingError(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent - data class ViewInTimeline(val eventId: EventId) : MediaViewerEvent - data class Forward(val eventId: EventId) : MediaViewerEvent - data class OpenInfo(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent +sealed interface MediaViewerEvents { + data class LoadMedia(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents + data class SaveOnDisk(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents + data class Share(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents + data class OpenWith(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents + data class ClearLoadingError(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents + data class ViewInTimeline(val eventId: EventId) : MediaViewerEvents + data class Forward(val eventId: EventId) : MediaViewerEvents + data class OpenInfo(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents data class ConfirmDelete( val eventId: EventId, val data: MediaViewerPageData.MediaViewerData, - ) : MediaViewerEvent + ) : MediaViewerEvents - data object CloseBottomSheet : 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 + data object CloseBottomSheet : MediaViewerEvents + data class Delete(val eventId: EventId) : MediaViewerEvents + data class OnNavigateTo(val index: Int) : MediaViewerEvents + data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvents } 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 e594daeb41..d834534e3e 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,7 +13,6 @@ 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 @@ -119,7 +118,6 @@ 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 296f20ef4c..dc0feb70cf 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,12 +14,14 @@ 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 @@ -43,7 +45,10 @@ 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.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import io.element.android.libraries.androidutils.R as UtilsR @@ -70,27 +75,12 @@ class MediaViewerPresenter( @Composable override fun present(): MediaViewerState { val coroutineScope = rememberCoroutineScope() - 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 data = dataSource.collectAsState() + val currentIndex = remember { mutableIntStateOf(searchIndex(data.value, inputs.eventId)) } val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() - // 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) + NoMoreItemsBackwardSnackBarDisplayer(currentIndex, data) + NoMoreItemsForwardSnackBarDisplayer(currentIndex, data) val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms -> perms.mediaPermissions() @@ -98,53 +88,50 @@ class MediaViewerPresenter( var mediaBottomSheetState by remember { mutableStateOf(MediaBottomSheetState.Hidden) } DisposableEffect(Unit) { - dataSource.setup(coroutineScope) + dataSource.setup() onDispose { dataSource.dispose() } } localMediaActions.Configure() - fun handleEvent(event: MediaViewerEvent) { + fun handleEvent(event: MediaViewerEvents) { when (event) { - is MediaViewerEvent.LoadMedia -> { + is MediaViewerEvents.LoadMedia -> { coroutineScope.downloadMedia(data = event.data) } - is MediaViewerEvent.CancelLoadingMedia -> { - dataSource.cancelLoadingMedia(event.data) - } - is MediaViewerEvent.ClearLoadingError -> { + is MediaViewerEvents.ClearLoadingError -> { dataSource.clearLoadingError(event.data) } - is MediaViewerEvent.SaveOnDisk -> { + is MediaViewerEvents.SaveOnDisk -> { mediaBottomSheetState = MediaBottomSheetState.Hidden coroutineScope.saveOnDisk(event.data.downloadedMedia.value) } - is MediaViewerEvent.Share -> { + is MediaViewerEvents.Share -> { mediaBottomSheetState = MediaBottomSheetState.Hidden coroutineScope.share(event.data.downloadedMedia.value) } - is MediaViewerEvent.OpenWith -> { + is MediaViewerEvents.OpenWith -> { mediaBottomSheetState = MediaBottomSheetState.Hidden coroutineScope.open(event.data.downloadedMedia.value) } - is MediaViewerEvent.Delete -> { + is MediaViewerEvents.Delete -> { mediaBottomSheetState = MediaBottomSheetState.Hidden coroutineScope.delete(event.eventId) } - is MediaViewerEvent.ViewInTimeline -> { + is MediaViewerEvents.ViewInTimeline -> { mediaBottomSheetState = MediaBottomSheetState.Hidden navigator.onViewInTimelineClick(event.eventId) } - is MediaViewerEvent.Forward -> { + is MediaViewerEvents.Forward -> { mediaBottomSheetState = MediaBottomSheetState.Hidden navigator.onForwardClick( eventId = event.eventId, fromPinnedEvents = inputs.mode.getTimelineMode() == Timeline.Mode.PinnedEvents, ) } - is MediaViewerEvent.OpenInfo -> coroutineScope.launch { - mediaBottomSheetState = MediaBottomSheetState.Details( + is MediaViewerEvents.OpenInfo -> coroutineScope.launch { + mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState( eventId = event.data.eventId, canDelete = when (event.data.mediaInfo.senderId) { null -> false @@ -155,20 +142,20 @@ class MediaViewerPresenter( thumbnailSource = event.data.thumbnailSource, ) } - is MediaViewerEvent.ConfirmDelete -> { - mediaBottomSheetState = MediaBottomSheetState.DeleteConfirmation( + is MediaViewerEvents.ConfirmDelete -> { + mediaBottomSheetState = MediaBottomSheetState.MediaDeleteConfirmationState( eventId = event.eventId, mediaInfo = event.data.mediaInfo, thumbnailSource = event.data.thumbnailSource ?: event.data.mediaSource, ) } - MediaViewerEvent.CloseBottomSheet -> { + MediaViewerEvents.CloseBottomSheet -> { mediaBottomSheetState = MediaBottomSheetState.Hidden } - is MediaViewerEvent.OnNavigateTo -> { + is MediaViewerEvents.OnNavigateTo -> { currentIndex.intValue = event.index } - is MediaViewerEvent.LoadMore -> coroutineScope.launch { + is MediaViewerEvents.LoadMore -> coroutineScope.launch { dataSource.loadMore(event.direction) } } @@ -186,39 +173,50 @@ class MediaViewerPresenter( } @Composable - private fun NoMoreItemsSnackBarDisplayer( + private fun NoMoreItemsBackwardSnackBarDisplayer( currentIndex: IntState, data: State>, - direction: Timeline.PaginationDirection, ) { - 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 - } + val isRenderingLoadingBackward by remember { + derivedStateOf { + currentIndex.intValue == data.value.lastIndex && + data.value.size > 1 && + data.value.lastOrNull() is MediaViewerPageData.Loading } - // 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 + } + if (isRenderingLoadingBackward) { + LaunchedEffect(Unit) { + // Observe the loading data vanishing + snapshotFlow { data.value.lastOrNull() is MediaViewerPageData.Loading } + .distinctUntilChanged() + .filter { !it } + .onEach { showNoMoreItemsSnackbar() } + .launchIn(this) } + } + } - val isLoading = isLoading(currentIndex.intValue, data.value, direction) - - if (wasLoading == true && !isLoading) { - showNoMoreItemsSnackbar() + @Composable + private fun NoMoreItemsForwardSnackBarDisplayer( + currentIndex: IntState, + data: State>, + ) { + val isRenderingLoadingForward by remember { + derivedStateOf { + currentIndex.intValue == 0 && + data.value.size > 1 && + data.value.firstOrNull() is MediaViewerPageData.Loading + } + } + if (isRenderingLoadingForward) { + LaunchedEffect(Unit) { + // Observe the loading data vanishing + snapshotFlow { data.value.firstOrNull() is MediaViewerPageData.Loading } + .distinctUntilChanged() + .filter { !it } + .onEach { showNoMoreItemsSnackbar() } + .launchIn(this) } - - previousDataSize = data.value.size - wasLoading = isLoading } } @@ -290,4 +288,13 @@ 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 a51d3c44e9..ae1a422a6f 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,7 +8,6 @@ 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 @@ -27,10 +26,9 @@ data class MediaViewerState( val snackbarMessage: SnackbarMessage?, val canShowInfo: Boolean, val mediaBottomSheetState: MediaBottomSheetState, - val eventSink: (MediaViewerEvent) -> Unit, + val eventSink: (MediaViewerEvents) -> 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 8cc14d6445..b1a2bde350 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt @@ -13,7 +13,6 @@ 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 @@ -26,8 +25,8 @@ import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState -import io.element.android.libraries.mediaviewer.impl.details.aMediaBottomSheetStateDeleteConfirmation -import io.element.android.libraries.mediaviewer.impl.details.aMediaBottomSheetStateDetails +import io.element.android.libraries.mediaviewer.impl.details.aMediaDeleteConfirmationState +import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState import kotlinx.collections.immutable.toImmutableList private const val LONG_CAPTION = "This is a very long caption that should be scrollable in the media viewer. " + @@ -142,10 +141,10 @@ open class MediaViewerStateProvider : PreviewParameterProvider ) }, aMediaViewerState( - mediaBottomSheetState = aMediaBottomSheetStateDetails(), + mediaBottomSheetState = aMediaDetailsBottomSheetState(), ), aMediaViewerState( - mediaBottomSheetState = aMediaBottomSheetStateDeleteConfirmation(), + mediaBottomSheetState = aMediaDeleteConfirmationState(), ), anAudioMediaInfo( waveForm = WaveFormSamples.realisticWaveForm, @@ -180,7 +179,7 @@ open class MediaViewerStateProvider : PreviewParameterProvider ) ), anImageMediaInfo( - senderName = USER_NAME_ALICE, + senderName = "Alice", dateSent = "21 NOV, 2024", caption = LONG_CAPTION, ).let { @@ -195,86 +194,6 @@ 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, - ) - ) - ) - }, ) } @@ -307,7 +226,7 @@ fun aMediaViewerState( currentIndex: Int = 0, canShowInfo: Boolean = true, mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden, - eventSink: (MediaViewerEvent) -> Unit = {}, + eventSink: (MediaViewerEvents) -> Unit = {}, ) = MediaViewerState( initiallySelectedEventId = EventId("\$a:b"), listData = listData.toImmutableList(), 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 3a7c006593..95ce7c631f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -31,11 +31,8 @@ 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 @@ -44,6 +41,7 @@ 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 @@ -51,12 +49,9 @@ 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 @@ -64,14 +59,13 @@ 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 import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.designsystem.components.async.AsyncFailure import io.element.android.libraries.designsystem.components.async.AsyncLoading @@ -91,6 +85,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.impl.R import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState import io.element.android.libraries.mediaviewer.impl.details.MediaDeleteConfirmationBottomSheet import io.element.android.libraries.mediaviewer.impl.details.MediaDetailsBottomSheet @@ -98,9 +93,7 @@ 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 @@ -108,9 +101,6 @@ import me.saket.telephoto.zoomable.rememberZoomableState val topAppBarHeight = 88.dp -/** - * Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3361-16623 - */ @Composable fun MediaViewerView( state: MediaViewerState, @@ -129,11 +119,94 @@ fun MediaViewerView( Scaffold( modifier, containerColor = Color.Transparent, - topBar = { - AnimatedVisibility( - visible = showOverlay, - enter = fadeIn(), - exit = fadeOut(), + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { + val pagerState = rememberPagerState(state.currentIndex, 0f) { + state.listData.size + } + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.currentPage }.collect { page -> + state.eventSink(MediaViewerEvents.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(MediaViewerEvents.LoadMore(dataForPage.direction)) + } + MediaViewerLoadingPage( + onDismiss = onBackClick, + ) + } + is MediaViewerPageData.MediaViewerData -> { + var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) } + LaunchedEffect(Unit) { + state.eventSink(MediaViewerEvents.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(MediaViewerEvents.LoadMedia(dataForPage)) + }, + onDismissError = { + state.eventSink(MediaViewerEvents.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() ) { when (currentData) { is MediaViewerPageData.MediaViewerData -> { @@ -141,15 +214,10 @@ fun MediaViewerView( data = currentData, canShowInfo = state.canShowInfo, onBackClick = onBackClick, - onShareClick = { - state.eventSink(MediaViewerEvent.Share(currentData)) - }, - onSaveClick = { - state.eventSink(MediaViewerEvent.SaveOnDisk(currentData)) - }, onInfoClick = { - state.eventSink(MediaViewerEvent.OpenInfo(currentData)) + state.eventSink(MediaViewerEvents.OpenInfo(currentData)) }, + eventSink = state.eventSink ) } else -> { @@ -174,130 +242,34 @@ 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)) - }, - 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 }, - ) - } - } - } - } } } when (val bottomSheetState = state.mediaBottomSheetState) { MediaBottomSheetState.Hidden -> Unit - is MediaBottomSheetState.Details -> { + is MediaBottomSheetState.MediaDetailsBottomSheetState -> { MediaDetailsBottomSheet( state = bottomSheetState, onViewInTimeline = { - state.eventSink(MediaViewerEvent.ViewInTimeline(it)) + state.eventSink(MediaViewerEvents.ViewInTimeline(it)) }, onShare = { (currentData as? MediaViewerPageData.MediaViewerData)?.let { - state.eventSink(MediaViewerEvent.Share(currentData)) + state.eventSink(MediaViewerEvents.Share(currentData)) } }, onForward = { - state.eventSink(MediaViewerEvent.Forward(it)) + state.eventSink(MediaViewerEvents.Forward(it)) }, onDownload = { (currentData as? MediaViewerPageData.MediaViewerData)?.let { - state.eventSink(MediaViewerEvent.SaveOnDisk(currentData)) - } - }, - onOpenWith = { - (currentData as? MediaViewerPageData.MediaViewerData)?.let { - state.eventSink(MediaViewerEvent.OpenWith(currentData)) + state.eventSink(MediaViewerEvents.SaveOnDisk(currentData)) } }, onDelete = { eventId -> (currentData as? MediaViewerPageData.MediaViewerData)?.let { state.eventSink( - MediaViewerEvent.ConfirmDelete( + MediaViewerEvents.ConfirmDelete( eventId, currentData, ) @@ -305,18 +277,18 @@ fun MediaViewerView( } }, onDismiss = { - state.eventSink(MediaViewerEvent.CloseBottomSheet) + state.eventSink(MediaViewerEvents.CloseBottomSheet) }, ) } - is MediaBottomSheetState.DeleteConfirmation -> { + is MediaBottomSheetState.MediaDeleteConfirmationState -> { MediaDeleteConfirmationBottomSheet( state = bottomSheetState, onDelete = { - state.eventSink(MediaViewerEvent.Delete(it)) + state.eventSink(MediaViewerEvents.Delete(it)) }, onDismiss = { - state.eventSink(MediaViewerEvent.CloseBottomSheet) + state.eventSink(MediaViewerEvents.CloseBottomSheet) }, ) } @@ -390,12 +362,11 @@ private fun MediaViewerPage( isUserSelected = isUserSelected, audioFocus = audioFocus, ) - if (showThumbnail) { - ThumbnailView( - mediaInfo = data.mediaInfo, - thumbnailSource = data.thumbnailSource, - ) - } + ThumbnailView( + mediaInfo = data.mediaInfo, + thumbnailSource = data.thumbnailSource, + isVisible = showThumbnail, + ) if (showError) { ErrorView( errorMessage = stringResource(id = CommonStrings.error_unknown), @@ -486,31 +457,25 @@ private fun MediaViewerTopBar( data: MediaViewerPageData.MediaViewerData, canShowInfo: Boolean, onBackClick: () -> Unit, - onShareClick: () -> Unit, - onSaveClick: () -> Unit, onInfoClick: () -> Unit, + eventSink: (MediaViewerEvents) -> Unit, ) { val downloadedMedia by data.downloadedMedia val actionsEnabled = downloadedMedia.isSuccess() + val mimeType = data.mediaInfo.mimeType val senderName = data.mediaInfo.senderName val dateSent = data.mediaInfo.dateSent 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, @@ -533,22 +498,21 @@ private fun MediaViewerTopBar( navigationIcon = { BackButton(onClick = onBackClick) }, actions = { IconButton( - onClick = onShareClick, enabled = actionsEnabled, + onClick = { + eventSink(MediaViewerEvents.OpenWith(data)) + }, ) { - Icon( - imageVector = CompoundIcons.ShareAndroid(), - contentDescription = stringResource(id = CommonStrings.action_share), - ) - } - IconButton( - onClick = onSaveClick, - enabled = actionsEnabled, - ) { - Icon( - imageVector = CompoundIcons.Download(), - contentDescription = stringResource(id = CommonStrings.action_download), - ) + when (mimeType) { + MimeTypes.Apk -> Icon( + resourceId = R.drawable.ic_apk_install, + contentDescription = stringResource(id = CommonStrings.common_install_apk_android) + ) + else -> Icon( + imageVector = CompoundIcons.PopOut(), + contentDescription = stringResource(id = CommonStrings.action_open_with) + ) + } } if (canShowInfo) { IconButton( @@ -568,7 +532,6 @@ private fun MediaViewerTopBar( @Composable private fun MediaViewerBottomBar( caption: String?, - formattedCaption: CharSequence?, showDivider: Boolean, onHeightChange: (Int) -> Unit, modifier: Modifier = Modifier, @@ -581,7 +544,7 @@ private fun MediaViewerBottomBar( onHeightChange(it.height) }, ) { - if (caption != null || formattedCaption != null) { + if (caption != null) { if (showDivider) { HorizontalDivider() } @@ -592,28 +555,15 @@ private fun MediaViewerBottomBar( .fillMaxWidth() .heightIn(max = if (hasCompactHeightWindowSize()) maxCaptionHeightLandscape else maxCaptionHeightPortrait), ) { - 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, - ) - } - } + Text( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .verticalScroll(scrollState) + .navigationBarsPadding(), + text = caption, + style = ElementTheme.typography.fontBodyLgRegular, + ) if (showBottomShadow) { Box( modifier = Modifier @@ -641,6 +591,7 @@ private val maxCaptionHeightLandscape = 128.dp @Composable private fun ThumbnailView( thumbnailSource: MediaSource?, + isVisible: Boolean, mediaInfo: MediaInfo, modifier: Modifier = Modifier, ) { @@ -648,19 +599,21 @@ private fun ThumbnailView( modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - 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, - ) + 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, + ) + } } } 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 146ace8620..f243ac4fd7 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,18 +20,15 @@ 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(coroutineScope: CoroutineScope) = Unit + override fun start() = 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-ca/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 44bf749c0f..0000000000 --- a/libraries/mediaviewer/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - "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 654aa869a6..bcb7b64237 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml @@ -16,7 +16,6 @@ "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 6108eab22d..5d88458495 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-da/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-da/translations.xml @@ -16,7 +16,6 @@ "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 f982b32433..63329a797d 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml @@ -16,7 +16,6 @@ "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 05154b293c..b3728cb9b7 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml @@ -16,7 +16,6 @@ "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 77372c1374..b6334095ab 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml @@ -16,7 +16,6 @@ "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 2a417cc070..7637fbf350 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-hr/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-hr/translations.xml @@ -16,7 +16,6 @@ "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 4f4d244d44..14cc11fb96 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml @@ -16,7 +16,6 @@ "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 913329122e..72701194fc 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml @@ -16,7 +16,6 @@ "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 9e136b0884..cb4da33df8 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml @@ -16,7 +16,6 @@ "ファイル名" "これ以上ファイルはありません" "これ以上メディアはありません" - "ファイル情報" "アップロード元" "アップロード先" 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 a1f7a3bdf9..c398123873 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-pl/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-pl/translations.xml @@ -16,7 +16,6 @@ "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 aa713361d0..bfd1d49c2b 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-ro/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-ro/translations.xml @@ -16,7 +16,6 @@ "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 74b910faaa..cdbe07f2e1 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml @@ -16,7 +16,6 @@ "Имя файла" "Больше нет файлов для отображения" "Больше нет медиа для отображения" - "Информация о файле" "Загружено" "Загружено" 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 ddc316fb4b..45e1e1703b 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-uk/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-uk/translations.xml @@ -16,7 +16,6 @@ "Назва файлу" "Більше немає файлів для показу" "Більше немає медіа для показу" - "Інформація про файл" "Вивантажено користувачем" "Вивантажено" diff --git a/libraries/mediaviewer/impl/src/main/res/values-zh/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-zh/translations.xml index 5bdec732a2..08b33993dc 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-zh/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-zh/translations.xml @@ -11,12 +11,11 @@ "媒体" "上传到此房间的图像和视频将在此处显示。" "尚未上传任何媒体" - "媒体与文件" + "媒体和文件" "文件格式" "文件名" "没有更多文件可显示了" "没有更多媒体可显示了" - "文件信息" "上传者:" "上传于" diff --git a/libraries/mediaviewer/impl/src/main/res/values/localazy.xml b/libraries/mediaviewer/impl/src/main/res/values/localazy.xml index 02a16a8736..2982f8002f 100644 --- a/libraries/mediaviewer/impl/src/main/res/values/localazy.xml +++ b/libraries/mediaviewer/impl/src/main/res/values/localazy.xml @@ -12,11 +12,10 @@ "Images and videos uploaded to this room will be shown here." "No media uploaded yet" "Media and files" - "Format" - "Name" + "File format" + "File name" "No more files to show" "No more media to show" - "File info" "Uploaded by" "Uploaded on" 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 e7d45f433a..b848ea2a7b 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,21 +33,16 @@ 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) @@ -94,7 +89,6 @@ 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 ab3ffa98c7..6c88f1c33f 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 @@ -18,7 +18,6 @@ import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.VideoInfo -import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent @@ -62,7 +61,7 @@ class DefaultEventItemFactoryTest { fun `create check all null cases`() { val factory = createEventItemFactory() val contents = listOf( - CallNotifyContent(callIntent = CallIntent.VIDEO, emptyList()), + CallNotifyContent, 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 5839f33ce6..c612bba1bc 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,22 +13,19 @@ 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.MutableStateFlow +import kotlinx.coroutines.flow.MutableSharedFlow 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(coroutineScope: CoroutineScope) = startLambda() + ) : MediaGalleryDataSource { + override fun start() = startLambda() - private val groupedMediaItemsFlow = MutableStateFlow(initialData) - - override val isReady: Boolean get() = isReadyResult() + private val groupedMediaItemsFlow = MutableSharedFlow>( + replay = 1 + ) 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 88aefb842c..bb8419dde5 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(backgroundScope) + sut.start() 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(backgroundScope) + sut.start() } } // Ensure that the timeline has been closed on flow completion @@ -117,7 +117,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start(backgroundScope) + sut.start() sut.groupedMediaItemsFlow().test { skipItems(2) sut.loadMore(Timeline.PaginationDirection.BACKWARDS) @@ -140,7 +140,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start(backgroundScope) + sut.start() sut.groupedMediaItemsFlow().test { skipItems(2) sut.deleteItem(AN_EVENT_ID) @@ -159,7 +159,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start(backgroundScope) + sut.start() sut.groupedMediaItemsFlow().test { assertThat(awaitItem().isLoading()).isTrue() assertThat(sut.getLastData().isLoading()).isTrue() @@ -181,7 +181,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start(backgroundScope) + sut.start() sut.groupedMediaItemsFlow().test { assertThat(awaitItem().isLoading()).isTrue() assertThat(sut.getLastData().isLoading()).isTrue() @@ -235,7 +235,6 @@ 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 3f4c3aa6c3..4d8b81a2dd 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,15 +6,12 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings @@ -24,39 +21,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.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() + @Test - fun `clicking on Cancel invokes expected callback`() = runAndroidComposeUiTest { - val state = aMediaBottomSheetStateDeleteConfirmation() + fun `clicking on Cancel invokes expected callback`() { + val state = aMediaDeleteConfirmationState() ensureCalledOnce { callback -> - setMediaDeleteConfirmationBottomSheet( + rule.setMediaDeleteConfirmationBottomSheet( state = state, onDismiss = callback, ) - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) } } @Test - fun `clicking on Remove invokes expected callback`() = runAndroidComposeUiTest { - val state = aMediaBottomSheetStateDeleteConfirmation() + fun `clicking on Remove invokes expected callback`() { + val state = aMediaDeleteConfirmationState() ensureCalledOnceWithParam(state.eventId) { callback -> - setMediaDeleteConfirmationBottomSheet( + rule.setMediaDeleteConfirmationBottomSheet( state = state, onDelete = callback, ) - onNodeWithText(activity!!.getString(CommonStrings.action_remove)).assertExists() - clickOn(CommonStrings.action_remove) + rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists() + rule.clickOn(CommonStrings.action_remove) } } } -private fun AndroidComposeUiTest.setMediaDeleteConfirmationBottomSheet( - state: MediaBottomSheetState.DeleteConfirmation, +private fun AndroidComposeTestRule.setMediaDeleteConfirmationBottomSheet( + state: MediaBottomSheetState.MediaDeleteConfirmationState, 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 5b0f105aea..580cd89c72 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,15 +6,12 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings @@ -23,98 +20,102 @@ 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.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() + @Test @Config(qualifiers = "h1024dp") - fun `clicking on View in timeline invokes expected callback`() = runAndroidComposeUiTest { - val state = aMediaBottomSheetStateDetails() + fun `clicking on View in timeline invokes expected callback`() { + val state = aMediaDetailsBottomSheetState() ensureCalledOnceWithParam(state.eventId) { callback -> - setMediaDetailsBottomSheet( + rule.setMediaDetailsBottomSheet( state = state, onViewInTimeline = callback, ) - clickOn(CommonStrings.action_view_in_timeline) + rule.clickOn(CommonStrings.action_view_in_timeline) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on Share invokes expected callback`() = runAndroidComposeUiTest { - val state = aMediaBottomSheetStateDetails() + fun `clicking on Share invokes expected callback`() { + val state = aMediaDetailsBottomSheetState() ensureCalledOnceWithParam(state.eventId) { callback -> - setMediaDetailsBottomSheet( + rule.setMediaDetailsBottomSheet( state = state, onShare = callback, ) - clickOn(CommonStrings.action_share) + rule.clickOn(CommonStrings.action_share) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on Forward invokes expected callback`() = runAndroidComposeUiTest { - val state = aMediaBottomSheetStateDetails() + fun `clicking on Forward invokes expected callback`() { + val state = aMediaDetailsBottomSheetState() ensureCalledOnceWithParam(state.eventId) { callback -> - setMediaDetailsBottomSheet( + rule.setMediaDetailsBottomSheet( state = state, onForward = callback, ) - clickOn(CommonStrings.action_forward) + rule.clickOn(CommonStrings.action_forward) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on Download invokes expected callback`() = runAndroidComposeUiTest { - val state = aMediaBottomSheetStateDetails() + fun `clicking on Save invokes expected callback`() { + val state = aMediaDetailsBottomSheetState() ensureCalledOnceWithParam(state.eventId) { callback -> - setMediaDetailsBottomSheet( + rule.setMediaDetailsBottomSheet( state = state, onDownload = callback, ) - clickOn(CommonStrings.action_download) + rule.clickOn(CommonStrings.action_save) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Delete invokes expected callback`() = runAndroidComposeUiTest { - val state = aMediaBottomSheetStateDetails() + fun `clicking on Remove invokes expected callback`() { + val state = aMediaDetailsBottomSheetState() ensureCalledOnceWithParam(state.eventId) { callback -> - setMediaDetailsBottomSheet( + rule.setMediaDetailsBottomSheet( state = state, onDelete = callback, ) - onNodeWithText(activity!!.getString(CommonStrings.action_delete)).assertExists() - clickOn(CommonStrings.action_delete) + rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists() + rule.clickOn(CommonStrings.action_remove) } } @Config(qualifiers = "h1024dp") @Test - fun `Remove is not present if canDelete is false`() = runAndroidComposeUiTest { - val state = aMediaBottomSheetStateDetails( + fun `Remove is not present if canDelete is false`() { + val state = aMediaDetailsBottomSheetState( canDelete = false, ) - setMediaDetailsBottomSheet( + rule.setMediaDetailsBottomSheet( state = state, ) - onNodeWithText(activity!!.getString(CommonStrings.action_remove)).assertDoesNotExist() + rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertDoesNotExist() } } -private fun AndroidComposeUiTest.setMediaDetailsBottomSheet( - state: MediaBottomSheetState.Details, +private fun AndroidComposeTestRule.setMediaDetailsBottomSheet( + state: MediaBottomSheetState.MediaDetailsBottomSheetState, onViewInTimeline: (EventId) -> Unit = EnsureNeverCalledWithParam(), onShare: (EventId) -> Unit = EnsureNeverCalledWithParam(), onForward: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDownload: (EventId) -> Unit = EnsureNeverCalledWithParam(), - onOpenWith: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { @@ -125,7 +126,6 @@ private fun AndroidComposeUiTest.setMediaDetailsBottomSheet( onShare = onShare, onForward = onForward, onDownload = onDownload, - onOpenWith = onOpenWith, onDelete = onDelete, onDismiss = onDismiss, ) 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..3069a4fd9d 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 @@ -6,8 +6,6 @@ * Please see LICENSE files in the repository root for full details. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package io.element.android.libraries.mediaviewer.impl.gallery import android.net.Uri @@ -29,7 +27,6 @@ import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.timeline.FakeTimeline -import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState @@ -42,8 +39,6 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -57,12 +52,8 @@ class MediaGalleryPresenterTest { @Test fun `present - initial state`() = runTest { - val configureLambda = lambdaRecorder { } val startLambda = lambdaRecorder { } val presenter = createMediaGalleryPresenter( - localMediaActions = FakeLocalMediaActions( - configureResult = configureLambda, - ), mediaGalleryDataSource = FakeMediaGalleryDataSource( startLambda = startLambda, ), @@ -79,7 +70,6 @@ class MediaGalleryPresenterTest { assertThat(initialState.groupedMediaItems.isUninitialized()).isTrue() assertThat(initialState.snackbarMessage).isNull() } - configureLambda.assertions().isCalledOnce() startLambda.assertions().isCalledOnce() } @@ -94,10 +84,10 @@ class MediaGalleryPresenterTest { presenter.test { val initialState = awaitFirstItem() assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images) - initialState.eventSink(MediaGalleryEvent.ChangeMode(MediaGalleryMode.Files)) + initialState.eventSink(MediaGalleryEvents.ChangeMode(MediaGalleryMode.Files)) val state = awaitItem() assertThat(state.mode).isEqualTo(MediaGalleryMode.Files) - state.eventSink(MediaGalleryEvent.ChangeMode(MediaGalleryMode.Images)) + state.eventSink(MediaGalleryEvents.ChangeMode(MediaGalleryMode.Images)) val imageModeState = awaitItem() assertThat(imageModeState.mode).isEqualTo(MediaGalleryMode.Images) } @@ -133,10 +123,10 @@ class MediaGalleryPresenterTest { eventId = AN_EVENT_ID, senderId = A_USER_ID, ) - initialState.eventSink(MediaGalleryEvent.OpenInfo(item)) + initialState.eventSink(MediaGalleryEvents.OpenInfo(item)) val state = awaitItem() assertThat(state.mediaBottomSheetState).isEqualTo( - MediaBottomSheetState.Details( + MediaBottomSheetState.MediaDetailsBottomSheetState( eventId = AN_EVENT_ID, canDelete = canDeleteOwn, mediaInfo = item.mediaInfo, @@ -144,7 +134,7 @@ class MediaGalleryPresenterTest { ) ) // Close the bottom sheet - state.eventSink(MediaGalleryEvent.CloseBottomSheet) + state.eventSink(MediaGalleryEvents.CloseBottomSheet) val closedState = awaitItem() assertThat(closedState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) } @@ -180,10 +170,10 @@ class MediaGalleryPresenterTest { eventId = AN_EVENT_ID, senderId = A_USER_ID_2, ) - initialState.eventSink(MediaGalleryEvent.OpenInfo(item)) + initialState.eventSink(MediaGalleryEvents.OpenInfo(item)) val state = awaitItem() assertThat(state.mediaBottomSheetState).isEqualTo( - MediaBottomSheetState.Details( + MediaBottomSheetState.MediaDetailsBottomSheetState( eventId = AN_EVENT_ID, canDelete = canDeleteOther, mediaInfo = item.mediaInfo, @@ -191,7 +181,7 @@ class MediaGalleryPresenterTest { ) ) // Close the bottom sheet - state.eventSink(MediaGalleryEvent.CloseBottomSheet) + state.eventSink(MediaGalleryEvents.CloseBottomSheet) val closedState = awaitItem() assertThat(closedState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) } @@ -209,17 +199,17 @@ class MediaGalleryPresenterTest { val initialState = awaitFirstItem() // Delete bottom sheet val item = aMediaItemImage() - initialState.eventSink(MediaGalleryEvent.ConfirmDelete(AN_EVENT_ID, item.mediaInfo, item.thumbnailSource)) + initialState.eventSink(MediaGalleryEvents.ConfirmDelete(AN_EVENT_ID, item.mediaInfo, item.thumbnailSource)) val deleteState = awaitItem() assertThat(deleteState.mediaBottomSheetState).isEqualTo( - MediaBottomSheetState.DeleteConfirmation( + MediaBottomSheetState.MediaDeleteConfirmationState( eventId = AN_EVENT_ID, mediaInfo = item.mediaInfo, thumbnailSource = item.thumbnailSource, ) ) // Close the bottom sheet - deleteState.eventSink(MediaGalleryEvent.CloseBottomSheet) + deleteState.eventSink(MediaGalleryEvents.CloseBottomSheet) val deleteClosedState = awaitItem() assertThat(deleteClosedState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) } @@ -236,7 +226,7 @@ class MediaGalleryPresenterTest { ) presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(MediaGalleryEvent.Delete(AN_EVENT_ID)) + initialState.eventSink(MediaGalleryEvents.Delete(AN_EVENT_ID)) deleteItemLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID)) } } @@ -246,7 +236,7 @@ class MediaGalleryPresenterTest { val presenter = createMediaGalleryPresenter() presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(MediaGalleryEvent.Share(AN_EVENT_ID)) + initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID)) } } @@ -268,7 +258,7 @@ class MediaGalleryPresenterTest { ) presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(MediaGalleryEvent.Share(AN_EVENT_ID)) + initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID)) val finalState = awaitItem() assertThat(finalState.snackbarMessage).isNull() } @@ -293,7 +283,7 @@ class MediaGalleryPresenterTest { ) presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(MediaGalleryEvent.Share(AN_EVENT_ID)) + initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID)) skipItems(1) val finalState = awaitItem() assertThat(finalState.snackbarMessage).isInstanceOf(SnackbarMessage::class.java) @@ -305,7 +295,7 @@ class MediaGalleryPresenterTest { val presenter = createMediaGalleryPresenter() presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(MediaGalleryEvent.SaveOnDisk(AN_EVENT_ID)) + initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID)) } } @@ -314,89 +304,23 @@ class MediaGalleryPresenterTest { val mediaGalleryDataSource = FakeMediaGalleryDataSource( startLambda = { }, ) - val saveOnDiskResult = lambdaRecorder> { _ -> Result.success(Unit) } - val media = aMediaItemImage(eventId = AN_EVENT_ID) mediaGalleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( - imageAndVideoItems = listOf(media), + imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)), fileItems = emptyList(), ) ) ) val presenter = createMediaGalleryPresenter( - localMediaActions = FakeLocalMediaActions( - saveOnDiskResult = saveOnDiskResult, - ), mediaGalleryDataSource = mediaGalleryDataSource, ) presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(MediaGalleryEvent.SaveOnDisk(AN_EVENT_ID)) + initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID)) skipItems(1) val finalState = awaitItem() assertThat(finalState.snackbarMessage?.messageResId).isEqualTo(CommonStrings.common_file_saved_on_disk_android) - saveOnDiskResult.assertions().isCalledOnce().with( - value( - LocalMedia( - uri = mockMediaUri, - info = media.mediaInfo, - ) - ) - ) - } - } - - @Test - fun `present - open with closes the bottom sheet and invokes the navigator`() = runTest { - val mediaGalleryDataSource = FakeMediaGalleryDataSource( - startLambda = { }, - ) - val openWithResult = lambdaRecorder> { _ -> Result.success(Unit) } - val item = aMediaItemImage( - eventId = AN_EVENT_ID, - senderId = A_USER_ID, - ) - mediaGalleryDataSource.emitGroupedMediaItems( - AsyncData.Success( - aGroupedMediaItems( - imageAndVideoItems = listOf(item), - fileItems = emptyList(), - ) - ) - ) - val presenter = createMediaGalleryPresenter( - localMediaActions = FakeLocalMediaActions( - openResult = openWithResult, - ), - mediaGalleryDataSource = mediaGalleryDataSource, - room = FakeJoinedRoom( - createTimelineResult = { Result.success(FakeTimeline()) }, - baseRoom = FakeBaseRoom( - roomPermissions = FakeRoomPermissions( - canRedactOwn = true - ), - ), - ), - ) - presenter.test { - skipItems(1) - val initialState = awaitFirstItem() - initialState.eventSink(MediaGalleryEvent.OpenInfo(item)) - val withBottomSheetState = awaitItem() - assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java) - withBottomSheetState.eventSink(MediaGalleryEvent.OpenWith(AN_EVENT_ID)) - val finalState = awaitItem() - assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) - advanceUntilIdle() - openWithResult.assertions().isCalledOnce().with( - value( - LocalMedia( - uri = mockMediaUri, - info = item.mediaInfo, - ) - ) - ) } } @@ -419,7 +343,7 @@ class MediaGalleryPresenterTest { ) presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(MediaGalleryEvent.SaveOnDisk(AN_EVENT_ID)) + initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID)) skipItems(1) val finalState = awaitItem() assertThat(finalState.snackbarMessage).isInstanceOf(SnackbarMessage::class.java) @@ -449,10 +373,10 @@ class MediaGalleryPresenterTest { eventId = AN_EVENT_ID, senderId = A_USER_ID, ) - initialState.eventSink(MediaGalleryEvent.OpenInfo(item)) + initialState.eventSink(MediaGalleryEvents.OpenInfo(item)) val withBottomSheetState = awaitItem() - assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java) - withBottomSheetState.eventSink(MediaGalleryEvent.ViewInTimeline(AN_EVENT_ID)) + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java) + withBottomSheetState.eventSink(MediaGalleryEvents.ViewInTimeline(AN_EVENT_ID)) val finalState = awaitItem() assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID)) @@ -482,10 +406,10 @@ class MediaGalleryPresenterTest { eventId = AN_EVENT_ID, senderId = A_USER_ID, ) - initialState.eventSink(MediaGalleryEvent.OpenInfo(item)) + initialState.eventSink(MediaGalleryEvents.OpenInfo(item)) val withBottomSheetState = awaitItem() - assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java) - withBottomSheetState.eventSink(MediaGalleryEvent.Forward(AN_EVENT_ID)) + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java) + withBottomSheetState.eventSink(MediaGalleryEvents.Forward(AN_EVENT_ID)) val finalState = awaitItem() assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) onForwardClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID)) @@ -503,7 +427,7 @@ class MediaGalleryPresenterTest { ) presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(MediaGalleryEvent.LoadMore(Timeline.PaginationDirection.BACKWARDS)) + initialState.eventSink(MediaGalleryEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS)) loadMoreLambda.assertions().isCalledOnce().with(value(Timeline.PaginationDirection.BACKWARDS)) } } 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 26f9087b39..f01ac1d749 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,9 +8,7 @@ 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 @@ -24,13 +22,9 @@ 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 @RunWith(RobolectricTestRunner::class) class AndroidLocalMediaFactoryTest { - private val context = RuntimeEnvironment.getApplication() - @Test fun `test AndroidLocalMediaFactory`() { val sut = createAndroidLocalMediaFactory() @@ -64,34 +58,13 @@ 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( - context, + RuntimeEnvironment.getApplication(), FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation() ) 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 d7ba547ee4..44eed2733f 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(backgroundScope) + sut.setup() startLambda.assertions().isCalledOnce() } @@ -62,20 +62,15 @@ class MediaViewerDataSourceTest { @Test fun `test dataFlow uninitialized, loading and error`() = runTest { - val galleryDataSource = FakeMediaGalleryDataSource( - initialData = AsyncData.Uninitialized, - ) + val galleryDataSource = FakeMediaGalleryDataSource() val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.dataFlow.test { - // The flow starts with an empty result - assertThat(awaitItem()).isEmpty() + sut.dataFlow().test { galleryDataSource.emitGroupedMediaItems(AsyncData.Uninitialized) assertThat(awaitItem().first()).isInstanceOf(MediaViewerPageData.Loading::class.java) galleryDataSource.emitGroupedMediaItems(AsyncData.Loading()) - // No items emitted, we were already loading data - ensureAllEventsConsumed() + assertThat(awaitItem().first()).isInstanceOf(MediaViewerPageData.Loading::class.java) galleryDataSource.emitGroupedMediaItems(AsyncData.Failure(AN_EXCEPTION)) assertThat(awaitItem().first()).isEqualTo(MediaViewerPageData.Failure(AN_EXCEPTION)) } @@ -87,7 +82,7 @@ class MediaViewerDataSourceTest { val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.dataFlow.test { + sut.dataFlow().test { galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -107,8 +102,7 @@ class MediaViewerDataSourceTest { val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.dataFlow.test { - skipItems(1) + sut.dataFlow().test { galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -147,8 +141,7 @@ class MediaViewerDataSourceTest { mode = MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.Media), galleryDataSource = galleryDataSource, ) - sut.dataFlow.test { - skipItems(1) + sut.dataFlow().test { galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -170,8 +163,7 @@ class MediaViewerDataSourceTest { mode = MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.Media), galleryDataSource = galleryDataSource, ) - sut.dataFlow.test { - skipItems(1) + sut.dataFlow().test { galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -192,8 +184,7 @@ class MediaViewerDataSourceTest { val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.dataFlow.test { - skipItems(1) + sut.dataFlow().test { galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -226,8 +217,7 @@ class MediaViewerDataSourceTest { val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.dataFlow.test { - skipItems(1) + sut.dataFlow().test { galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -251,8 +241,7 @@ class MediaViewerDataSourceTest { galleryDataSource = galleryDataSource, mediaLoader = mediaLoader, ) - sut.dataFlow.test { - skipItems(1) + sut.dataFlow().test { galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -282,7 +271,6 @@ 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 459d30b8d5..a9d1704bdc 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,7 +21,6 @@ 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 @@ -52,8 +51,6 @@ 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 @@ -100,8 +97,6 @@ class MediaViewerPresenterTest { assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) - - cancelAndIgnoreRemainingEvents() } } @@ -125,8 +120,6 @@ class MediaViewerPresenterTest { assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.canShowInfo).isFalse() assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) - - cancelAndIgnoreRemainingEvents() } } @@ -150,8 +143,6 @@ class MediaViewerPresenterTest { assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) - - cancelAndIgnoreRemainingEvents() } } @@ -176,8 +167,6 @@ class MediaViewerPresenterTest { assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) - - cancelAndIgnoreRemainingEvents() } } @@ -237,7 +226,7 @@ class MediaViewerPresenterTest { ) val updatedState = awaitItem() updatedState.eventSink( - MediaViewerEvent.LoadMedia( + MediaViewerEvents.LoadMedia( aMediaViewerPageData( mediaSource = MediaSource(aUrl) ) @@ -277,16 +266,16 @@ class MediaViewerPresenterTest { ) val updatedState = awaitItem() updatedState.eventSink( - MediaViewerEvent.OpenInfo( + MediaViewerEvents.OpenInfo( aMediaViewerPageData( mediaSource = MediaSource(aUrl) ) ) ) val withInfoState = awaitItem() - assertThat(withInfoState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java) + assertThat(withInfoState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java) withInfoState.eventSink( - MediaViewerEvent.CloseBottomSheet + MediaViewerEvents.CloseBottomSheet ) val finalState = awaitItem() assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) @@ -317,7 +306,7 @@ class MediaViewerPresenterTest { ) val updatedState = awaitItem() updatedState.eventSink( - MediaViewerEvent.ClearLoadingError( + MediaViewerEvents.ClearLoadingError( aMediaViewerPageData( mediaSource = MediaSource(aUrl) ) @@ -350,7 +339,7 @@ class MediaViewerPresenterTest { ) val updatedState = awaitItem() updatedState.eventSink( - MediaViewerEvent.Share( + MediaViewerEvents.Share( aMediaViewerPageData( mediaSource = MediaSource(aUrl) ) @@ -383,7 +372,7 @@ class MediaViewerPresenterTest { ) val updatedState = awaitItem() updatedState.eventSink( - MediaViewerEvent.SaveOnDisk( + MediaViewerEvents.SaveOnDisk( aMediaViewerPageData( mediaSource = MediaSource(aUrl) ) @@ -416,7 +405,7 @@ class MediaViewerPresenterTest { ) val updatedState = awaitItem() updatedState.eventSink( - MediaViewerEvent.OpenWith( + MediaViewerEvents.OpenWith( aMediaViewerPageData( mediaSource = MediaSource(aUrl) ) @@ -449,7 +438,7 @@ class MediaViewerPresenterTest { ) val updatedState = awaitItem() updatedState.eventSink( - MediaViewerEvent.ConfirmDelete( + MediaViewerEvents.ConfirmDelete( eventId = AN_EVENT_ID, data = aMediaViewerPageData( mediaSource = MediaSource(aUrl) @@ -457,9 +446,9 @@ class MediaViewerPresenterTest { ) ) val withBottomSheetState = awaitItem() - assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.DeleteConfirmation::class.java) + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDeleteConfirmationState::class.java) withBottomSheetState.eventSink( - MediaViewerEvent.CloseBottomSheet + MediaViewerEvents.CloseBottomSheet ) val finalState = awaitItem() assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) @@ -509,7 +498,7 @@ class MediaViewerPresenterTest { ) val updatedState = awaitItem() updatedState.eventSink( - MediaViewerEvent.ConfirmDelete( + MediaViewerEvents.ConfirmDelete( eventId = AN_EVENT_ID, data = aMediaViewerPageData( mediaSource = MediaSource(aUrl) @@ -517,9 +506,9 @@ class MediaViewerPresenterTest { ) ) val withBottomSheetState = awaitItem() - assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.DeleteConfirmation::class.java) + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDeleteConfirmationState::class.java) updatedState.eventSink( - MediaViewerEvent.Delete( + MediaViewerEvents.Delete( eventId = AN_EVENT_ID, ) ) @@ -562,12 +551,10 @@ class MediaViewerPresenterTest { ) val updatedState = awaitItem() updatedState.eventSink( - MediaViewerEvent.OnNavigateTo(1) + MediaViewerEvents.OnNavigateTo(1) ) val finalState = awaitItem() assertThat(finalState.currentIndex).isEqualTo(1) - - cancelAndIgnoreRemainingEvents() } } @@ -591,37 +578,35 @@ 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 { - val updatedState = awaitFirstItem() - - advanceUntilIdle() - runCurrent() + awaitFirstItem() + mediaGalleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { + GroupedMediaItems( + imageAndVideoItems = persistentListOf(), + fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator), + ) + } else { + GroupedMediaItems( + imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator), + fileItems = persistentListOf(), + ) + } + ) + ) + val updatedState = awaitItem() // User navigate to the first item (forward loading indicator) updatedState.eventSink( - MediaViewerEvent.OnNavigateTo(0) + MediaViewerEvents.OnNavigateTo(0) ) // data source claims that there is no more items to load forward mediaGalleryDataSource.emitGroupedMediaItems( @@ -629,17 +614,17 @@ class MediaViewerPresenterTest { if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), - fileItems = persistentListOf(image, aBackwardLoadingIndicator), + fileItems = persistentListOf(anImage, aBackwardLoadingIndicator), ) } else { GroupedMediaItems( - imageAndVideoItems = persistentListOf(image, aBackwardLoadingIndicator), + imageAndVideoItems = persistentListOf(anImage, aBackwardLoadingIndicator), fileItems = persistentListOf(), ) } ) ) - skipItems(2) + skipItems(1) val stateWithSnackbar = awaitItem() assertThat(stateWithSnackbar.snackbarMessage!!.messageResId).isEqualTo(expectedSnackbarResId) } @@ -693,7 +678,7 @@ class MediaViewerPresenterTest { val updatedState = awaitItem() // User navigate to the last item (backward loading indicator) updatedState.eventSink( - MediaViewerEvent.OnNavigateTo(2) + MediaViewerEvents.OnNavigateTo(2) ) skipItems(1) // data source claims that there is no more items to load backward @@ -722,23 +707,25 @@ 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(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator), + fileItems = persistentListOf(), + ) + ) + ) val updatedState = awaitItem() // User navigate to the media updatedState.eventSink( - MediaViewerEvent.OnNavigateTo(1) + MediaViewerEvents.OnNavigateTo(1) ) skipItems(1) // data source claims that there is no more items to load at all @@ -781,57 +768,12 @@ class MediaViewerPresenterTest { ) val updatedState = awaitItem() updatedState.eventSink( - MediaViewerEvent.LoadMore(Timeline.PaginationDirection.BACKWARDS) + MediaViewerEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS) ) loadMoreLambda.assertions().isCalledOnce().with(value(Timeline.PaginationDirection.BACKWARDS)) } } - @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 { } @@ -851,11 +793,10 @@ class MediaViewerPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(MediaViewerEvent.OpenInfo(aMediaViewerPageData())) - skipItems(1) + initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData())) val withBottomSheetState = awaitItem() - assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java) - initialState.eventSink(MediaViewerEvent.ViewInTimeline(AN_EVENT_ID)) + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java) + initialState.eventSink(MediaViewerEvents.ViewInTimeline(AN_EVENT_ID)) val finalState = awaitItem() assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID)) @@ -881,11 +822,10 @@ class MediaViewerPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(MediaViewerEvent.OpenInfo(aMediaViewerPageData())) - skipItems(1) + initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData())) val withBottomSheetState = awaitItem() - assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java) - initialState.eventSink(MediaViewerEvent.Forward(AN_EVENT_ID)) + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java) + initialState.eventSink(MediaViewerEvents.Forward(AN_EVENT_ID)) val finalState = awaitItem() assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) onForwardClickLambda.assertions().isCalledOnce() @@ -913,11 +853,10 @@ class MediaViewerPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(MediaViewerEvent.OpenInfo(aMediaViewerPageData())) - skipItems(1) + initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData())) val withBottomSheetState = awaitItem() - assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java) - initialState.eventSink(MediaViewerEvent.Forward(AN_EVENT_ID)) + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java) + initialState.eventSink(MediaViewerEvents.Forward(AN_EVENT_ID)) val finalState = awaitItem() assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) onForwardClickLambda.assertions().isCalledOnce() @@ -953,7 +892,6 @@ 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 6521f450f5..b1114945c2 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,30 +6,20 @@ * 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.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 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.impl.details.aMediaDetailsBottomSheetState import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -39,97 +29,81 @@ import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.setSafeContent import io.mockk.mockk -import kotlinx.coroutines.delay +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 MediaViewerViewTest { + @get:Rule val rule = createAndroidComposeRule() + private val mockMediaUrl: Uri = mockk("localMediaUri") @Test - fun `clicking on back invokes expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking on back invokes expected callback`() { + val eventsRecorder = EventsRecorder() val state = aMediaViewerState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - setMediaViewerView( + rule.setMediaViewerView( state = state, onBackClick = callback, ) - - // Wait for enough time for the onVisibilityChanged modifier to trigger - mainClock.advanceTimeBy(200) - - pressBack() + rule.pressBack() } eventsRecorder.assertList( listOf( - MediaViewerEvent.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData), + MediaViewerEvents.OnNavigateTo(0), + MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData), ) ) } @Test - fun `clicking on info emits expected Event`() { + fun `clicking on open emit expected Event`() { + val data = aMediaViewerPageData( + downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)), + ) + testMenuAction( + data, + CommonStrings.action_open_with, + MediaViewerEvents.OpenWith(data), + ) + } + + @Test + fun `clicking on info emit expected Event`() { val data = aMediaViewerPageData( downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)), ) testMenuAction( data, CommonStrings.a11y_view_details, - MediaViewerEvent.OpenInfo(data), - ) - } - - @Test - fun `clicking on top action share emits expected Event`() { - val data = aMediaViewerPageData( - downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)), - ) - testMenuAction( - data, - CommonStrings.action_share, - MediaViewerEvent.Share(data), - ) - } - - @Test - fun `clicking on top action download emits expected Event`() { - val data = aMediaViewerPageData( - downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl)), - ) - testMenuAction( - data, - CommonStrings.action_download, - MediaViewerEvent.SaveOnDisk(data), + MediaViewerEvents.OpenInfo(data), ) } private fun testMenuAction( data: MediaViewerPageData.MediaViewerData, - @StringRes contentDescriptionRes: Int, - expectedEvent: MediaViewerEvent, - ) = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() - setMediaViewerView( + contentDescriptionRes: Int, + expectedEvent: MediaViewerEvents, + ) { + val eventsRecorder = EventsRecorder() + rule.setMediaViewerView( aMediaViewerState( listData = listOf(data), eventSink = eventsRecorder ), ) - - // Wait for enough time for the onVisibilityChanged modifier to trigger - mainClock.advanceTimeBy(200) - - val contentDescription = activity!!.getString(contentDescriptionRes) - onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = rule.activity.getString(contentDescriptionRes) + rule.onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertList( listOf( - MediaViewerEvent.LoadMedia(data), + MediaViewerEvents.OnNavigateTo(0), + MediaViewerEvents.LoadMedia(data), expectedEvent, ) ) @@ -137,215 +111,146 @@ class MediaViewerViewTest { @Test @Config(qualifiers = "h1024dp") - fun `clicking on download emits expected Event`() { + fun `clicking on save emit expected Event`() { val data = aMediaViewerPageData() testBottomSheetAction( data, - CommonStrings.action_download, - MediaViewerEvent.SaveOnDisk(data), + CommonStrings.action_save, + MediaViewerEvents.SaveOnDisk(data), ) } @Test @Config(qualifiers = "h1024dp") - fun `clicking on share emits expected Event`() { + fun `clicking on share emit 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`() { - val data = aMediaViewerPageData() - testBottomSheetAction( - data, - CommonStrings.action_open_with, - MediaViewerEvent.OpenWith(data), + MediaViewerEvents.Share(data), ) } private fun testBottomSheetAction( data: MediaViewerPageData.MediaViewerData, - @StringRes textRes: Int, - expectedEvent: MediaViewerEvent, - ) = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() - setMediaViewerView( + contentDescriptionRes: Int, + expectedEvent: MediaViewerEvents, + ) { + val eventsRecorder = EventsRecorder() + rule.setMediaViewerView( aMediaViewerState( listData = listOf(data), - mediaBottomSheetState = aMediaBottomSheetStateDetails(), + mediaBottomSheetState = aMediaDetailsBottomSheetState(), eventSink = eventsRecorder ), ) - clickOn(textRes) + rule.clickOn(contentDescriptionRes) eventsRecorder.assertList( listOf( - MediaViewerEvent.LoadMedia(data), + MediaViewerEvents.OnNavigateTo(0), + MediaViewerEvents.LoadMedia(data), expectedEvent, ) ) } @Test - fun `clicking on image hides the overlay`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking on image hides the overlay`() { + val eventsRecorder = EventsRecorder() val state = aMediaViewerState( eventSink = eventsRecorder ) - setMediaViewerView( + rule.setMediaViewerView( state = state, ) // Ensure that the action are visible - val resources = activity!!.resources - val contentDescription = resources.getString(CommonStrings.action_share) - onNodeWithContentDescription(contentDescription) + val contentDescription = rule.activity.getString(CommonStrings.action_open_with) + rule.onNodeWithContentDescription(contentDescription) .assertExists() .assertHasClickAction() - val imageContentDescription = resources.getString(CommonStrings.common_image) - onNodeWithContentDescription(imageContentDescription).performClick() + val imageContentDescription = rule.activity.getString(CommonStrings.common_image) + rule.onNodeWithContentDescription(imageContentDescription).performClick() // Give time for the animation (? since even by removing AnimatedVisibility it still fails) - mainClock.advanceTimeBy(1_000) - onNodeWithContentDescription(contentDescription) + rule.mainClock.advanceTimeBy(1_000) + rule.onNodeWithContentDescription(contentDescription) .assertDoesNotExist() eventsRecorder.assertList( listOf( - MediaViewerEvent.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData), + MediaViewerEvents.OnNavigateTo(0), + MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData), ) ) } @Test - fun `clicking swipe on the image invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking swipe on the image invokes the expected callback`() { + val eventsRecorder = EventsRecorder() val state = aMediaViewerState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - setMediaViewerView( + rule.setMediaViewerView( state = state, onBackClick = callback, ) - val imageContentDescription = activity!!.getString(CommonStrings.common_image) - onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) } - mainClock.advanceTimeBy(1_000) + val imageContentDescription = rule.activity.getString(CommonStrings.common_image) + rule.onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) } + rule.mainClock.advanceTimeBy(1_000) } eventsRecorder.assertList( listOf( - MediaViewerEvent.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData), + MediaViewerEvents.OnNavigateTo(0), + MediaViewerEvents.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData), ) ) } @Test - fun `error case, click on retry emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `error case, click on retry emits the expected Event`() { + val eventsRecorder = EventsRecorder() val data = aMediaViewerPageData( downloadedMedia = AsyncData.Failure(IllegalStateException("error")), ) - setMediaViewerView( + rule.setMediaViewerView( aMediaViewerState( listData = listOf(data), eventSink = eventsRecorder ), ) - - // Wait for enough time for the onVisibilityChanged modifier to trigger - mainClock.advanceTimeBy(200) - - clickOn(CommonStrings.action_retry) + rule.clickOn(CommonStrings.action_retry) eventsRecorder.assertList( listOf( - MediaViewerEvent.LoadMedia(data), - MediaViewerEvent.LoadMedia(data), + MediaViewerEvents.OnNavigateTo(0), + MediaViewerEvents.LoadMedia(data), + MediaViewerEvents.LoadMedia(data), ) ) } @Test - fun `error case, click on cancel emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `error case, click on cancel emits the expected Event`() { + val eventsRecorder = EventsRecorder() val data = aMediaViewerPageData( downloadedMedia = AsyncData.Failure(IllegalStateException("error")), ) - setMediaViewerView( + rule.setMediaViewerView( aMediaViewerState( listData = listOf(data), eventSink = eventsRecorder ), ) - - // Wait for enough time for the onVisibilityChanged modifier to trigger - mainClock.advanceTimeBy(200) - - clickOn(CommonStrings.action_cancel) + rule.clickOn(CommonStrings.action_cancel) eventsRecorder.assertList( listOf( - 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), + MediaViewerEvents.OnNavigateTo(0), + MediaViewerEvents.LoadMedia(data), + MediaViewerEvents.ClearLoadingError(data) ) ) } } -private fun AndroidComposeUiTest.setMediaViewerView( +private fun AndroidComposeTestRule.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 8c0a7c05d0..c6460cb70a 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`() = runTest { + fun `function start is no op`() { val sut = SingleMediaGalleryDataSource(aGroupedMediaItems()) - sut.start(backgroundScope) + sut.start() } @Test diff --git a/libraries/mediaviewer/test/build.gradle.kts b/libraries/mediaviewer/test/build.gradle.kts index 87665e6d69..1918714d7b 100644 --- a/libraries/mediaviewer/test/build.gradle.kts +++ b/libraries/mediaviewer/test/build.gradle.kts @@ -18,7 +18,6 @@ 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/FakeLocalMediaActions.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaActions.kt index d048f73eab..875be941db 100644 --- a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaActions.kt +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaActions.kt @@ -11,29 +11,37 @@ package io.element.android.libraries.mediaviewer.test import androidx.compose.runtime.Composable import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions -import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask -class FakeLocalMediaActions( - val configureResult: () -> Unit = { }, - val saveOnDiskResult: (LocalMedia) -> Result = { lambdaError() }, - val shareResult: (LocalMedia) -> Result = { lambdaError() }, - val openResult: (LocalMedia) -> Result = { lambdaError() }, -) : LocalMediaActions { +class FakeLocalMediaActions : LocalMediaActions { + var shouldFail = false + @Composable override fun Configure() { - configureResult() + // NOOP } override suspend fun saveOnDisk(localMedia: LocalMedia): Result = simulateLongTask { - saveOnDiskResult(localMedia) + if (shouldFail) { + Result.failure(RuntimeException()) + } else { + Result.success(Unit) + } } override suspend fun share(localMedia: LocalMedia): Result = simulateLongTask { - shareResult(localMedia) + if (shouldFail) { + Result.failure(RuntimeException()) + } else { + Result.success(Unit) + } } override suspend fun open(localMedia: LocalMedia): Result = simulateLongTask { - openResult(localMedia) + if (shouldFail) { + Result.failure(RuntimeException()) + } else { + Result.success(Unit) + } } } 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 8dbee232df..faa27fd0e3 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,7 +41,6 @@ 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/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt index 39a54dca61..88240c3d65 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,6 +9,7 @@ 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 @@ -18,8 +19,8 @@ import retrofit2.converter.kotlinx.serialization.asConverterFactory @Inject class RetrofitFactory( - private val okHttpClient: () -> OkHttpClient, - private val json: () -> JsonProvider, + private val okHttpClient: Provider, + private val json: Provider, ) { fun create(baseUrl: String): Retrofit = Retrofit.Builder() .baseUrl(baseUrl.ensureTrailingSlash()) 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 deleted file mode 100644 index c2a29e228c..0000000000 --- a/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthIntentResolver.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.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/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 deleted file mode 100644 index 5a5ca4369e..0000000000 --- a/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/customtab/FakeOAuthActionFlow.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.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/oauth/api/build.gradle.kts b/libraries/oidc/api/build.gradle.kts similarity index 87% rename from libraries/oauth/api/build.gradle.kts rename to libraries/oidc/api/build.gradle.kts index 8c863db0bf..8cc0125142 100644 --- a/libraries/oauth/api/build.gradle.kts +++ b/libraries/oidc/api/build.gradle.kts @@ -11,7 +11,7 @@ plugins { } android { - namespace = "io.element.android.libraries.oauth.api" + namespace = "io.element.android.libraries.oidc.api" } dependencies { diff --git a/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthAction.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt similarity index 54% rename from libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthAction.kt rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt index 033516b93d..d7c061ab25 100644 --- a/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthAction.kt +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt @@ -6,9 +6,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oauth.api +package io.element.android.libraries.oidc.api -sealed interface OAuthAction { - data class GoBack(val toUnblock: Boolean = false) : OAuthAction - data class Success(val url: String) : OAuthAction +sealed interface OidcAction { + data class GoBack(val toUnblock: Boolean = false) : OidcAction + data class Success(val url: String) : OidcAction } diff --git a/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthActionFlow.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt similarity index 63% rename from libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthActionFlow.kt rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt index c6791e3197..17340eb5ec 100644 --- a/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthActionFlow.kt +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt @@ -6,12 +6,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oauth.api +package io.element.android.libraries.oidc.api import kotlinx.coroutines.flow.FlowCollector -interface OAuthActionFlow { - fun post(oAuthAction: OAuthAction) - suspend fun collect(collector: FlowCollector) +interface OidcActionFlow { + fun post(oidcAction: OidcAction) + suspend fun collect(collector: FlowCollector) fun reset() } diff --git a/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthIntentResolver.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt similarity index 68% rename from libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthIntentResolver.kt rename to libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt index 2091a86db4..97fa1baa27 100644 --- a/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthIntentResolver.kt +++ b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt @@ -6,10 +6,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oauth.api +package io.element.android.libraries.oidc.api import android.content.Intent -interface OAuthIntentResolver { - fun resolve(intent: Intent): OAuthAction? +interface OidcIntentResolver { + fun resolve(intent: Intent): OidcAction? } diff --git a/libraries/oauth/impl/build.gradle.kts b/libraries/oidc/impl/build.gradle.kts similarity index 93% rename from libraries/oauth/impl/build.gradle.kts rename to libraries/oidc/impl/build.gradle.kts index d051c06497..e11ce11c70 100644 --- a/libraries/oauth/impl/build.gradle.kts +++ b/libraries/oidc/impl/build.gradle.kts @@ -16,7 +16,7 @@ plugins { } android { - namespace = "io.element.android.libraries.oauth.impl" + namespace = "io.element.android.libraries.oidc.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.oauth.api) + api(projects.libraries.oidc.api) testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) diff --git a/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlow.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt similarity index 58% rename from libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlow.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt index 6b23676059..6096ef7eef 100644 --- a/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlow.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt @@ -6,26 +6,26 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oauth.impl +package io.element.android.libraries.oidc.impl import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn -import io.element.android.libraries.oauth.api.OAuthAction -import io.element.android.libraries.oauth.api.OAuthActionFlow +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 @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultOAuthActionFlow : OAuthActionFlow { - private val mutableStateFlow = MutableStateFlow(null) +class DefaultOidcActionFlow : OidcActionFlow { + private val mutableStateFlow = MutableStateFlow(null) - override fun post(oAuthAction: OAuthAction) { - mutableStateFlow.value = oAuthAction + override fun post(oidcAction: OidcAction) { + mutableStateFlow.value = oidcAction } - override suspend fun collect(collector: FlowCollector) { + override suspend fun collect(collector: FlowCollector) { mutableStateFlow.collect(collector) } 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 new file mode 100644 index 0000000000..2a16030b3b --- /dev/null +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.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.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/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/OAuthUrlParser.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt similarity index 51% rename from libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/OAuthUrlParser.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt index e62dea696f..8933873dc2 100644 --- a/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/OAuthUrlParser.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt @@ -6,37 +6,37 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oauth.impl +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.OAuthRedirectUrlProvider -import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider +import io.element.android.libraries.oidc.api.OidcAction -fun interface OAuthUrlParser { - fun parse(url: String): OAuthAction? +fun interface OidcUrlParser { + fun parse(url: String): OidcAction? } /** - * Simple parser for OAuth url interception. + * Simple parser for oidc url interception. * TODO Find documentation about the format. */ @ContributesBinding(AppScope::class) -class DefaultOAuthUrlParser( - private val oAuthRedirectUrlProvider: OAuthRedirectUrlProvider, -) : OAuthUrlParser { +class DefaultOidcUrlParser( + private val oidcRedirectUrlProvider: OidcRedirectUrlProvider, +) : OidcUrlParser { /** - * Return a [OAuthAction], or null if the url is not an OAuth url. + * 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): 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) + 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/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlowTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt similarity index 58% rename from libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlowTest.kt rename to libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt index 817487b3ff..387b9aceb0 100644 --- a/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlowTest.kt +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt @@ -1,33 +1,34 @@ /* - * Copyright (c) 2026 Element Creations Ltd. + * 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.impl +package io.element.android.libraries.oidc.impl import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oidc.api.OidcAction import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Test -class DefaultOAuthActionFlowTest { +class DefaultOidcActionFlowTest { @Test fun `collect gets all the posted events`() = runTest { - val data = mutableListOf() - val sut = DefaultOAuthActionFlow() + val data = mutableListOf() + val sut = DefaultOidcActionFlow() backgroundScope.launch { sut.collect { action -> data.add(action) } } - sut.post(OAuthAction.GoBack()) + sut.post(OidcAction.GoBack()) delay(1) sut.reset() delay(1) - assertThat(data).containsExactly(OAuthAction.GoBack(), null) + assertThat(data).containsExactly(OidcAction.GoBack(), null) } } diff --git a/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthIntentResolverTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolverTest.kt similarity index 64% rename from libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthIntentResolverTest.kt rename to libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolverTest.kt index ff34ae8220..64068030d7 100644 --- a/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthIntentResolverTest.kt +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolverTest.kt @@ -1,18 +1,19 @@ /* - * Copyright (c) 2026 Element Creations Ltd. + * 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.impl +package io.element.android.libraries.oidc.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.FakeOAuthRedirectUrlProvider -import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider +import io.element.android.libraries.oidc.api.OidcAction import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith @@ -20,36 +21,36 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) -class DefaultOAuthIntentResolverTest { +class DefaultOidcIntentResolverTest { @Test - fun `test resolve OAuth go back`() { - val sut = createDefaultOAuthIntentResolver() + fun `test resolve oidc go back`() { + val sut = createDefaultOidcIntentResolver() 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(OAuthAction.GoBack()) + assertThat(result).isEqualTo(OidcAction.GoBack()) } @Test - fun `test resolve OAuth success`() { - val sut = createDefaultOAuthIntentResolver() + fun `test resolve oidc success`() { + val sut = createDefaultOidcIntentResolver() 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( - OAuthAction.Success( + OidcAction.Success( url = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" ) ) } @Test - fun `test resolve OAuth invalid`() { - val sut = createDefaultOAuthIntentResolver() + fun `test resolve oidc invalid`() { + val sut = createDefaultOidcIntentResolver() val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW data = "io.element.android:/invalid".toUri() @@ -59,10 +60,10 @@ class DefaultOAuthIntentResolverTest { } } - private fun createDefaultOAuthIntentResolver(): DefaultOAuthIntentResolver { - return DefaultOAuthIntentResolver( - oAuthUrlParser = DefaultOAuthUrlParser( - oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(), + private fun createDefaultOidcIntentResolver(): DefaultOidcIntentResolver { + return DefaultOidcIntentResolver( + oidcUrlParser = DefaultOidcUrlParser( + oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), ), ) } diff --git a/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthUrlParserTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcUrlParserTest.kt similarity index 56% rename from libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthUrlParserTest.kt rename to libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcUrlParserTest.kt index 6377414862..7f145c053d 100644 --- a/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthUrlParserTest.kt +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcUrlParserTest.kt @@ -1,58 +1,59 @@ /* - * Copyright (c) 2026 Element Creations Ltd. + * 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 +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.FakeOAuthRedirectUrlProvider -import io.element.android.libraries.oauth.api.OAuthAction +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 DefaultOAuthUrlParserTest { +class DefaultOidcUrlParserTest { @Test fun `test empty url`() { - val sut = createDefaultOAuthUrlParser() + val sut = createDefaultOidcUrlParser() assertThat(sut.parse("")).isNull() } @Test fun `test regular url`() { - val sut = createDefaultOAuthUrlParser() + val sut = createDefaultOidcUrlParser() assertThat(sut.parse("https://matrix.org")).isNull() } @Test fun `test cancel url`() { - val sut = createDefaultOAuthUrlParser() + val sut = createDefaultOidcUrlParser() val aCancelUrl = "$FAKE_REDIRECT_URL?error=access_denied&state=IFF1UETGye2ZA8pO" - assertThat(sut.parse(aCancelUrl)).isEqualTo(OAuthAction.GoBack()) + assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack()) } @Test fun `test success url`() { - val sut = createDefaultOAuthUrlParser() + val sut = createDefaultOidcUrlParser() val aSuccessUrl = "$FAKE_REDIRECT_URL?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" - assertThat(sut.parse(aSuccessUrl)).isEqualTo(OAuthAction.Success(aSuccessUrl)) + assertThat(sut.parse(aSuccessUrl)).isEqualTo(OidcAction.Success(aSuccessUrl)) } @Test fun `test unknown url`() { - val sut = createDefaultOAuthUrlParser() + val sut = createDefaultOidcUrlParser() val anUnknownUrl = "$FAKE_REDIRECT_URL?state=IFF1UETGye2ZA8pO&goat=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" Assert.assertThrows(IllegalStateException::class.java) { assertThat(sut.parse(anUnknownUrl)) } } - private fun createDefaultOAuthUrlParser(): DefaultOAuthUrlParser { - return DefaultOAuthUrlParser( - oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(), + private fun createDefaultOidcUrlParser(): DefaultOidcUrlParser { + return DefaultOidcUrlParser( + oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), ) } } diff --git a/libraries/oauth/test/build.gradle.kts b/libraries/oidc/test/build.gradle.kts similarity index 80% rename from libraries/oauth/test/build.gradle.kts rename to libraries/oidc/test/build.gradle.kts index 6850653ddc..efe32d404a 100644 --- a/libraries/oauth/test/build.gradle.kts +++ b/libraries/oidc/test/build.gradle.kts @@ -11,11 +11,11 @@ plugins { } android { - namespace = "io.element.android.libraries.oauth.test" + namespace = "io.element.android.libraries.oidc.test" } dependencies { implementation(libs.coroutines.core) - api(projects.libraries.oauth.api) + api(projects.libraries.oidc.api) implementation(projects.tests.testutils) } diff --git a/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/FakeOAuthIntentResolver.kt b/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/FakeOidcIntentResolver.kt similarity index 50% rename from libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/FakeOAuthIntentResolver.kt rename to libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/FakeOidcIntentResolver.kt index 893289023e..45b400868b 100644 --- a/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/FakeOAuthIntentResolver.kt +++ b/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/FakeOidcIntentResolver.kt @@ -6,17 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oauth.test +package io.element.android.libraries.oidc.test import android.content.Intent -import io.element.android.libraries.oauth.api.OAuthAction -import io.element.android.libraries.oauth.api.OAuthIntentResolver +import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oidc.api.OidcIntentResolver import io.element.android.tests.testutils.lambda.lambdaError -class FakeOAuthIntentResolver( - private val resolveResult: (Intent) -> OAuthAction? = { lambdaError() } -) : OAuthIntentResolver { - override fun resolve(intent: Intent): OAuthAction? { +class FakeOidcIntentResolver( + private val resolveResult: (Intent) -> OidcAction? = { lambdaError() } +) : OidcIntentResolver { + override fun resolve(intent: Intent): OidcAction? { return resolveResult(intent) } } 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 new file mode 100644 index 0000000000..5362aefa7c --- /dev/null +++ b/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/customtab/FakeOidcActionFlow.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.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 deleted file mode 100644 index 0b8527649b..0000000000 --- a/libraries/permissions/api/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - "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/api/src/main/res/values-zh/translations.xml b/libraries/permissions/api/src/main/res/values-zh/translations.xml index a071113a1f..eb093046a3 100644 --- a/libraries/permissions/api/src/main/res/values-zh/translations.xml +++ b/libraries/permissions/api/src/main/res/values-zh/translations.xml @@ -1,7 +1,7 @@ - "为了让 app 使用相机,请在系统设置中授予权限。" + "为了让应用程序使用相机,请在系统设置中授予权限。" "请在系统设置中授予权限。" - "为了让 app 使用麦克风,请在系统设置中授予权限。" - "为了让 app 显示通知,请在系统设置中授予权限。" + "为了让应用程序使用麦克风,请在系统设置中授予权限。" + "为了让应用程序显示通知,请在系统设置中授予权限。" diff --git a/libraries/permissions/impl/src/main/res/values-ca/translations.xml b/libraries/permissions/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 206c416d8b..0000000000 --- a/libraries/permissions/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - "Comprova que l\'aplicació pot mostrar notificacions." - "Comprova els permisos" - 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 df8dc8acc9..476658946a 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,9 +23,6 @@ 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.") diff --git a/libraries/preferences/impl/build.gradle.kts b/libraries/preferences/impl/build.gradle.kts index 73327a69a2..c567471da4 100644 --- a/libraries/preferences/impl/build.gradle.kts +++ b/libraries/preferences/impl/build.gradle.kts @@ -1,5 +1,4 @@ import extension.setupDependencyInjection -import extension.testCommonDependencies /* * Copyright (c) 2025 Element Creations Ltd. @@ -26,7 +25,4 @@ 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 44260461da..6856f8bdb6 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,7 +10,6 @@ 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 @@ -29,7 +28,6 @@ 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") @@ -81,18 +79,6 @@ 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 -> 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 deleted file mode 100644 index c52d1648ac..0000000000 --- a/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt +++ /dev/null @@ -1,57 +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.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 152de12e99..6e7d22a568 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 @@ -21,14 +21,12 @@ class InMemoryAppPreferencesStore( hideInviteAvatars: Boolean? = null, timelineMediaPreviewValue: MediaPreviewValue? = null, theme: String? = null, - liveLocationMinimumDistanceUpdate: Int = 10, logLevel: LogLevel = LogLevel.INFO, traceLockPacks: Set = emptySet(), ) : 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 hideInviteAvatars = MutableStateFlow(hideInviteAvatars) @@ -58,14 +56,6 @@ 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 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 49f77ddefb..1b73ccea31 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,16 +8,6 @@ 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 @@ -33,7 +23,6 @@ fun aRoomMember( isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, - isServiceMember: Boolean = false, ) = RoomMember( userId = userId, displayName = displayName, @@ -44,33 +33,32 @@ fun aRoomMember( isIgnored = isIgnored, role = role, membershipChangeReason = membershipChangeReason, - isServiceMember = isServiceMember, ) fun aRoomMemberList() = persistentListOf( anAlice(), aBob(), - 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), + 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"), aVictor(), aWalter(), ) -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 anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.Admin) +fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.Moderator) fun aVictor() = aRoomMember( UserId("@victor:server.org"), - USER_NAME_VICTOR, + "Victor", membership = RoomMembershipState.INVITE ) fun aWalter() = aRoomMember( UserId("@walter:server.org"), - USER_NAME_WALTER, + "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 6f86789693..4bf1d2501b 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,7 +8,6 @@ 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 @@ -20,7 +19,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, @@ -34,7 +33,6 @@ fun aSpaceRoom( topic: String? = null, worldReadable: Boolean = false, isDirect: Boolean? = null, - isDm: Boolean? = null, via: List = emptyList(), ) = SpaceRoom( rawName = rawName, @@ -52,6 +50,5 @@ fun aSpaceRoom( topic = topic, worldReadable = worldReadable, via = via.toImmutableList(), - isDirect = isDirect, - isDm = isDm, + isDirect = isDirect ) 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 367052eaad..ff7119b647 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,5 +53,4 @@ 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/conversations/NotificationConversationService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/conversations/NotificationConversationService.kt index 4ea80e4ce8..504adacdb6 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/push/FetchPushForegroundServiceManager.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/FetchPushForegroundServiceManager.kt deleted file mode 100644 index be178bbe6c..0000000000 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/FetchPushForegroundServiceManager.kt +++ /dev/null @@ -1,27 +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.push.api.push - -/** - * A helper to manage the foreground service used to keep the device awake while we schedule and wait for the work to fetch the notification content to run. - */ -interface FetchPushForegroundServiceManager { - /** - * Start the foreground service to acquire the wakelock. If the device is already awake, this method does nothing. - * - * @return true if the service was started, false otherwise (e.g. if the device was already awake or if starting the service failed). - */ - fun start(): Boolean - - /** - * Stop the foreground service to release the wakelock. If the service is not running, this method does nothing. - * - * @return true if the service was stopped, false otherwise (e.g. if the service was not running or if stopping the service failed). - */ - suspend fun stop(): Boolean -} diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/PushHandlingWakeLock.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/PushHandlingWakeLock.kt new file mode 100644 index 0000000000..5c76eb1864 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/push/PushHandlingWakeLock.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.push + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes + +/** + * Abstraction over wakelocks used for push handling to ensure the device stays awake while we handle the push and schedule and run the work. + */ +interface PushHandlingWakeLock { + /** + * Acquire a wakelock. The wakelock will be held for the given [time] or until [unlock] is called, whichever happens first. + */ + fun lock(time: Duration = 1.minutes) + + /** + * Release the wakelock. If no wakelock is associated with the key, this method does nothing. + */ + suspend fun unlock() +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/di/LocationBindings.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindings.kt similarity index 58% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/di/LocationBindings.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindings.kt index ee70936160..49b2c43bc7 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/di/LocationBindings.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindings.kt @@ -5,13 +5,13 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.location.impl.di +package io.element.android.libraries.push.impl.di import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesTo -import io.element.android.features.location.impl.live.service.LiveLocationSharingService +import io.element.android.libraries.push.impl.push.FetchPushForegroundService @ContributesTo(AppScope::class) -interface LocationBindings { - fun inject(service: LiveLocationSharingService) +interface PushBindings { + fun inject(fetchPushForegroundService: FetchPushForegroundService) } 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 cb9ef8c82d..eb08a25c6a 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,6 +19,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.room.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 a97937f5d0..e6a3201cbf 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,9 +10,11 @@ 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.CallData +import io.element.android.features.call.api.CallType import io.element.android.features.call.api.ElementCallEntryPoint 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.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId @@ -30,6 +32,7 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived import io.element.android.libraries.push.impl.push.OnRedactedEventReceived +import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent import io.element.android.libraries.pushstore.api.UserPushStoreFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -57,6 +60,8 @@ class DefaultNotificationResultProcessor( private val userPushStoreFactory: UserPushStoreFactory, private val onRedactedEventReceived: OnRedactedEventReceived, private val onNotifiableEventReceived: OnNotifiableEventReceived, + private val featureFlagService: FeatureFlagService, + private val syncOnNotifiableEvent: SyncOnNotifiableEvent, private val elementCallEntryPoint: ElementCallEntryPoint, private val notificationChannels: NotificationChannels, @AppCoroutineScope private val coroutineScope: CoroutineScope, @@ -210,14 +215,18 @@ class DefaultNotificationResultProcessor( if (nonRingingCallEvents.isNotEmpty()) { onNotifiableEventReceived.onNotifiableEventsReceived(nonRingingCallEvents) } + + if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncNotificationsWithWorkManager)) { + syncOnNotifiableEvent(results.keys.toList()) + } } private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) { Timber.i("## handleInternal() : Incoming call.") elementCallEntryPoint.handleIncomingCall( - callData = CallData( - sessionId = notifiableEvent.sessionId, - roomId = notifiableEvent.roomId, + callType = CallType.RoomCall( + notifiableEvent.sessionId, + 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/conversations/DefaultNotificationConversationService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt index f898e4a9ed..ce20234385 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,11 +93,10 @@ 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 = name, + name = roomName, url = roomAvatarUrl, size = AvatarSize.RoomDetailsHeader, ), @@ -106,7 +105,7 @@ class DefaultNotificationConversationService( )?.let(IconCompat::createWithBitmap) val shortcutInfo = ShortcutInfoCompat.Builder(context, createShortcutId(sessionId, roomId)) - .setShortLabel(name) + .setShortLabel(roomName) .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/push/DefaultFetchPushForegroundServiceManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultFetchPushForegroundServiceManager.kt deleted file mode 100644 index 968661268a..0000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultFetchPushForegroundServiceManager.kt +++ /dev/null @@ -1,95 +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.push.impl.push - -import android.app.ActivityManager -import android.content.Context -import android.content.Context.ACTIVITY_SERVICE -import android.content.Context.POWER_SERVICE -import android.content.Intent -import android.os.Build -import android.os.PowerManager -import androidx.core.content.ContextCompat -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding -import dev.zacsweers.metro.SingleIn -import io.element.android.libraries.core.extensions.runCatchingExceptions -import io.element.android.libraries.di.annotations.ApplicationContext -import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager -import kotlinx.coroutines.delay -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withTimeoutOrNull -import timber.log.Timber -import kotlin.time.Duration.Companion.seconds - -@ContributesBinding(AppScope::class) -@SingleIn(AppScope::class) -class DefaultFetchPushForegroundServiceManager( - @ApplicationContext private val context: Context, -) : FetchPushForegroundServiceManager { - private val stopMutex = Mutex() - - override fun start(): Boolean { - Timber.d("Acquiring wakelock for push handling, starting service.") - - // Don't start the foreground service if the device is already awake - val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager - if (powerManager.isInteractive) { - Timber.d("Device is already in an interactive state, no need to start FetchPushForegroundService") - return false - } - - val intent = Intent(context, FetchPushForegroundService::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - runCatchingExceptions { ContextCompat.startForegroundService(context, intent) } - .onFailure { throwable -> - Timber.e(throwable, "Failed to start FetchPushForegroundService, notifications may take longer than usual to sync") - } - } else { - context.startService(intent) - } - - return true - } - - override suspend fun stop(): Boolean { - Timber.d("Releasing wakelock used for push handling, stopping service.") - return stopMutex.withLock { - val runningServiceInfo = getRunningServiceInfo(context) - if (runningServiceInfo != null) { - val intent = Intent(context, FetchPushForegroundService::class.java) - // If it's still not running in foreground, it means the service is still starting, - // so we delay the stop to give it time to start and be set as foreground, otherwise we can crash - // with `ForegroundServiceDidNotStartInTimeException`. - var isInForeground = runningServiceInfo.foreground - withTimeoutOrNull(5.seconds) { - while (!isInForeground) { - delay(50) - val updatedServiceInfo = getRunningServiceInfo(context) - if (updatedServiceInfo == null) { - Timber.d("FetchPushForegroundService is no longer running, no need to stop it.") - return@withTimeoutOrNull - } - isInForeground = updatedServiceInfo.foreground == true - } - } ?: Timber.w("FetchPushForegroundService did not start in foreground after 5s, stopping it anyway.") - context.stopService(intent) - } else { - false - } - } - } - - @Suppress("DEPRECATION") - private fun getRunningServiceInfo(context: Context): ActivityManager.RunningServiceInfo? { - val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager - return activityManager.getRunningServices(Int.MAX_VALUE) - .firstOrNull { it.service.className == FetchPushForegroundService::class.java.name } - } -} 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 d5c3f04348..44cf6edefc 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,7 +11,6 @@ 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 @@ -36,7 +35,6 @@ 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) @@ -55,7 +53,6 @@ class DefaultPushHandler( private val workManagerScheduler: WorkManagerScheduler, private val syncPendingNotificationsRequestFactory: SyncPendingNotificationsRequestBuilder.Factory, resultProcessor: NotificationResultProcessor, - private val dispatchers: CoroutineDispatchers, ) : PushHandler { init { resultProcessor.start() @@ -67,7 +64,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 = withContext(dispatchers.computation) { + override suspend fun handle(pushData: PushData, providerInfo: String): Boolean { // 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)) @@ -84,7 +81,7 @@ class DefaultPushHandler( } // Diagnostic Push - if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) { + return if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) { pushHistoryService.onDiagnosticPush(providerInfo) diagnosticPushHandler.handlePush() false @@ -93,7 +90,7 @@ class DefaultPushHandler( } } - override suspend fun handleInvalid(providerInfo: String, data: String) = withContext(dispatchers.computation) { + override suspend fun handleInvalid(providerInfo: String, data: String) { incrementPushDataStore.incrementPushCounter() pushHistoryService.onInvalidPushReceived(providerInfo, data) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt new file mode 100644 index 0000000000..27a921c219 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.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.push.impl.push + +import android.content.Context +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.android.libraries.push.api.push.PushHandlingWakeLock +import timber.log.Timber +import kotlin.time.Duration + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class DefaultPushHandlingWakeLock( + @ApplicationContext private val context: Context, +) : PushHandlingWakeLock { + override fun lock(time: Duration) { + Timber.d("Acquiring wakelock for push handling, starting service.") + FetchPushForegroundService.startIfNeeded(context) + } + + override suspend fun unlock() { + Timber.d("Releasing wakelock used for push handling.") + FetchPushForegroundService.stop(context) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt index b1c90a374a..d54b7f5497 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt @@ -7,36 +7,38 @@ package io.element.android.libraries.push.impl.push +import android.app.ActivityManager import android.app.Service +import android.content.Context import android.content.Intent -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.api.push.PushHandlingWakeLock +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.MainScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds 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. */ @@ -45,35 +47,29 @@ class FetchPushForegroundService : Service() { return null } + @Inject lateinit var notificationChannels: NotificationChannels + @Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock + @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, WAKELOCK_TAG).apply { + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "FetchPushService:WakeLock").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.i("Creating FetchPushForegroundService to handle incoming push, acquiring wakelock for up to $wakelockTimeout ms") - ensureNotificationChannelExists() + Timber.d("Creating FetchPushForegroundService") + 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, CHANNEL_ID) + val notificationCompat = NotificationCompat.Builder(this, notificationChannels.getSilentChannelId()) .setSmallIcon(CommonDrawables.ic_notification) - .setContentTitle(getString(CommonStrings.common_fetching_notifications_title_android).ifEmpty { "Syncing notifications…" }) + .setContentTitle(getString(CommonStrings.common_android_fetching_notifications_title)) .setProgress(0, 0, true) .setVibrate(longArrayOf(0)) .setSound(null) @@ -82,13 +78,8 @@ class FetchPushForegroundService : Service() { // Try to start the service in foreground. This can fail, even in cases where it's supposed to work according to the docs. // In those cases we catch the exception and handle the failure so we don't try to start the wakelock or stop the service // from running in foreground later. - val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE - } else { - 0 - } runCatchingExceptions { - ServiceCompat.startForeground(this, NOTIFICATION_ID, notificationCompat, serviceType) + startForeground(NOTIFICATION_ID, notificationCompat) } .onSuccess { isOnForeground = true @@ -113,7 +104,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) { - MainScope().launch { + coroutineScope.launch { delay(wakelockTimeout) onTimeoutAction(calledByTheSystem = false) } @@ -122,18 +113,13 @@ class FetchPushForegroundService : Service() { return START_NOT_STICKY } - override fun onDestroy() { - super.onDestroy() - + override fun stopService(intent: Intent?): Boolean { 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") + wakelock.release() + stopForeground(STOP_FOREGROUND_REMOVE) } + + return super.stopService(intent) } override fun onTimeout(startId: Int) { @@ -142,10 +128,67 @@ class FetchPushForegroundService : Service() { } private fun onTimeoutAction(calledByTheSystem: Boolean) { - Timber.w("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground") + Timber.d("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground") if (isOnForeground) { - Timber.w("Wakelock timeout reached, stopping FetchPushForegroundService") - stopSelf() + Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService") + coroutineScope.launch { pushHandlingWakeLock.unlock() } + } + } + + companion object { + private val stopMutex = Mutex() + + fun startIfNeeded(context: Context) { + // Don't start the foreground service if the device is already awake + val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager + if (powerManager.isInteractive) return + + start(context) + } + + fun start(context: Context) { + val intent = Intent(context, FetchPushForegroundService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + runCatchingExceptions { context.startForegroundService(intent) } + .onFailure { throwable -> + Timber.e( + throwable, + "Failed to start FetchPushForegroundService, notifications may take longer than usual to sync" + ) + } + } else { + context.startService(intent) + } + } + + suspend fun stop(context: Context) = stopMutex.withLock { + val runningServiceInfo = getRunningServiceInfo(context) + if (runningServiceInfo != null) { + val intent = Intent(context, FetchPushForegroundService::class.java) + // If it's still not running in foreground, it means the service is still starting, + // so we delay the stop to give it time to start and be set as foreground, otherwise we can crash + // with `ForegroundServiceDidNotStartInTimeException`. + var isInForeground = runningServiceInfo.foreground + withTimeoutOrNull(5.seconds) { + while (!isInForeground) { + delay(50) + val updatedServiceInfo = getRunningServiceInfo(context) + if (updatedServiceInfo == null) { + Timber.d("FetchPushForegroundService is no longer running, no need to stop it.") + return@withTimeoutOrNull + } + isInForeground = updatedServiceInfo.foreground == true + } + } ?: Timber.w("FetchPushForegroundService did not start in foreground after 5s, stopping it anyway.") + context.stopService(intent) + } + } + + @Suppress("DEPRECATION") + private fun getRunningServiceInfo(context: Context): ActivityManager.RunningServiceInfo? { + val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager + return activityManager.getRunningServices(Int.MAX_VALUE) + .firstOrNull { it.service.className == FetchPushForegroundService::class.java.name } } } } 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 cefbd31515..ec57582529 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 @@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.api.auth.SessionRestorationException import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.matrix.api.exception.isNetworkError -import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager +import io.element.android.libraries.push.api.push.PushHandlingWakeLock import io.element.android.libraries.push.impl.db.PushRequest import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver @@ -58,7 +58,7 @@ class FetchPendingNotificationsWorker( private val resultProcessor: NotificationResultProcessor, private val analyticsService: AnalyticsService, private val systemClock: SystemClock, - private val fetchPushForegroundServiceManager: FetchPushForegroundServiceManager, + private val pushHandlingWakeLock: PushHandlingWakeLock, ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { Timber.d("FetchNotificationsWorker started") @@ -67,8 +67,7 @@ class FetchPendingNotificationsWorker( inputData.getString(SyncPendingNotificationsRequestBuilder.SESSION_ID)?.let(::SessionId) }.getOrNull() ?: return Result.failure() - // We can stop the foreground service and unlock the wakelock, since the work is now running and the device should be kept awake - fetchPushForegroundServiceManager.stop() + pushHandlingWakeLock.unlock() // Fetch pending requests in the last 24 hours val fetchSince = Instant.fromEpochMilliseconds(systemClock.epochMillis()).minus(1.days) @@ -153,7 +152,7 @@ class FetchPendingNotificationsWorker( private suspend fun checkNetworkConnection(requests: List): Result? { val networkTimeoutSpans = requests.mapNotNull { request -> - val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(request.eventId)) + val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(request.eventId)) parent?.startChild("Waiting for network connectivity", "await_network") } diff --git a/libraries/push/impl/src/main/res/values-ca/translations.xml b/libraries/push/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index 56655d250d..0000000000 --- a/libraries/push/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - "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-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml index 60e03373f1..6c0e51564a 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -15,11 +15,6 @@ "Der Dienst für UnifiedPush Benachrichtigungen konnte nicht registriert werden. Daher können aktuell keine Push-Benachrichtigungen erhalten werden. Bitte überprüfe die Einstellungen der Benachrichtigungen in der App und den Status des Push-Dienstes." "Du hast neue Nachrichten." - - "Du hast %d neue Nachricht." - "Du hast %d neue Nachrichten." - - "📞 Eingehender Anruf" "Eingehender Anruf" "** Fehler beim Senden - bitte Chat öffnen" "Beitreten" 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 49010359cd..4b00b7b5e7 100644 --- a/libraries/push/impl/src/main/res/values-et/translations.xml +++ b/libraries/push/impl/src/main/res/values-et/translations.xml @@ -19,8 +19,7 @@ "Sul on %d uus sõnum." "Sul on %d uut sõnumit." - "📞 Saabuv kõne" - "📹 Saabuv kõne" + "📹 Sissetulev kõne" "** Saatmine ei õnnestunud - palun ava jututoa täisvaade" "Liitu" "Keeldu" diff --git a/libraries/push/impl/src/main/res/values-hr/translations.xml b/libraries/push/impl/src/main/res/values-hr/translations.xml index eead0db104..86c458dbf9 100644 --- a/libraries/push/impl/src/main/res/values-hr/translations.xml +++ b/libraries/push/impl/src/main/res/values-hr/translations.xml @@ -17,11 +17,6 @@ "Distributer obavijesti UnifiedPush nije mogao biti registriran, tako da više nećete primati obavijesti. Provjerite postavke obavijesti u aplikaciji i status distributera push obavijesti." "Imate nove poruke." - - "Imate%d novu poruku." - "Imate %d novi poruka." - - "📞 Dolazni poziv" "📹 Dolazni poziv" "** Slanje nije uspjelo – otvorite sobu" "Pridruži se" 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 d8a4077d05..b9ae289507 100644 --- a/libraries/push/impl/src/main/res/values-pl/translations.xml +++ b/libraries/push/impl/src/main/res/values-pl/translations.xml @@ -15,14 +15,7 @@ "%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" @@ -48,15 +41,12 @@ "%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ści" + "%d nieprzeczytane wiadomość" "%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 2f677568ea..0a83e6afad 100644 --- a/libraries/push/impl/src/main/res/values-ro/translations.xml +++ b/libraries/push/impl/src/main/res/values-ro/translations.xml @@ -15,16 +15,10 @@ "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ți" + "Respinge" "%d invitație" "%d invitații" 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 9c5f74605f..d3aee22165 100644 --- a/libraries/push/impl/src/main/res/values-uk/translations.xml +++ b/libraries/push/impl/src/main/res/values-uk/translations.xml @@ -15,14 +15,7 @@ "%d сповіщення" "%d сповіщень" - "Розподільник сповіщень UnifiedPush не вдалося зареєструвати, тому ви більше не отримуватимете сповіщень. Перевірте налаштування сповіщень у додатку та статус розподільника push-сповіщень." "У вас є нові повідомлення." - - "У вас %d нове повідомлення." - "У вас %d нових повідомлення." - "У вас %d нових повідомлень." - - "📞 Вхідний дзвінок" "📹 Вхідний виклик" "** Не вдалося надіслати - відкрийте кімнату" "Доєднатися" @@ -48,7 +41,6 @@ "%1$s запросив вас приєднатися до кімнати" "Я" "%1$s згадували або відповідали" - "Вас запросили приєднатися до простору" "%1$s запрошує вас приєднатися до простору" "Ви переглядаєте сповіщення! Натисніть тут!" "Гілка в %1$s" @@ -70,18 +62,11 @@ "Фонова синхронізація" "Сервіси 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-uz/translations.xml b/libraries/push/impl/src/main/res/values-uz/translations.xml index d4a018ab80..1058fdd1d2 100644 --- a/libraries/push/impl/src/main/res/values-uz/translations.xml +++ b/libraries/push/impl/src/main/res/values-uz/translations.xml @@ -15,11 +15,6 @@ "UnifiedPush bildirishnoma tarqatuvchisini roʻyxatdan oʻtkazib boʻlmadi, shuning uchun siz endi bildirishnomalarni olmaysiz. Iltimos, ilovaning bildirishnoma sozlamalarini va push distribyutor holatini tekshiring." "Sizda yangi xabarlar bor." - - "Sizda %d ta yangi xabar bor." - "Sizda %d ta yangi xabar bor." - - "📞 Kiruvchi qo‘ng‘iroq" "📹 Kiruvchi qoʻngʻiroq" "** Yuborilmadi - iltimos, xonani oching" "Qo\'shilish" diff --git a/libraries/push/impl/src/main/res/values-vi/translations.xml b/libraries/push/impl/src/main/res/values-vi/translations.xml index b286c8fc29..5592ff0e3a 100644 --- a/libraries/push/impl/src/main/res/values-vi/translations.xml +++ b/libraries/push/impl/src/main/res/values-vi/translations.xml @@ -11,12 +11,7 @@ "%d thông báo" - "Không thể đăng ký trình phân phối thông báo UnifiedPush, vì vậy bạn sẽ không nhận được thông báo nữa. Vui lòng kiểm tra cài đặt thông báo của ứng dụng và trạng thái của trình phân phối thông báo." "Bạn có tin nhắn mới." - - "Bạn có %d tin nhắn mới." - - "📞 Cuộc gọi đến" "📹 Cuộc gọi đến" "** Không gửi được - vui lòng mở phòng" "Tham gia" @@ -25,7 +20,6 @@ "%d lời mời" "Đã mời bạn trò chuyện" - "%1$s đã mời bạn trò chuyện" "Đã nhắc đến bạn: %1$s" "Tin nhắn mới" @@ -35,13 +29,8 @@ "Đánh dấu đã đọc" "Trả lời nhanh" "Đã mời bạn tham gia phòng" - "%1$s đã mời bạn tham gia phòng chat" "Tôi" - "%1$s đã đề cập hoặc trả lời" - "Mời bạn tham gia không gian này." - "%1$s đã mời bạn tham gia không gian này." "Bạn đang xem thông báo! Bấm vào đây!" - "Chuỗi cuộc trò chuyện trong: %1$s" "%1$s:%2$s" "%1$s: %2$s %3$s" @@ -56,9 +45,6 @@ "Đồng bộ hóa trong nền" "Dịch vụ của Google" "Không tìm thấy Dịch vụ Google Play hợp lệ. Thông báo có thể không hoạt động đúng cách." - - "Bạn đã chặn người dùng %1$d. Bạn sẽ không nhận được thông báo từ người này." - "Người dùng bị chặn" "Hãy đảm bảo rằng ứng dụng hỗ trợ ít nhất một nhà cung cấp thông báo đẩy." "Không tìm thấy hỗ trợ từ nhà cung cấp thông báo đẩy." diff --git a/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml index 207f962b90..2d289e9a9e 100644 --- a/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml @@ -13,10 +13,6 @@ "Unified Push 通知散佈程式註冊失敗,因此您無法再收到通知。請檢查應用程式的通知設定與推播散佈程式的狀態。" "您有新訊息。" - - "您有 %d 則新訊息。" - - "📞 來電" "📹 來電" "** 無法傳送,請開啟聊天室" "加入" @@ -38,8 +34,6 @@ "%1$s 邀請您加入聊天室" "我" "%1$s 提及或回覆" - "已邀請您加入空間" - "%1$s 已邀請您的加入此空間" "您正在查看通知!點我!" "在 %1$s 的討論串" "%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 7532f16ba0..7e59c317c3 100644 --- a/libraries/push/impl/src/main/res/values-zh/translations.xml +++ b/libraries/push/impl/src/main/res/values-zh/translations.xml @@ -1,74 +1,74 @@ "通话" - "正在监听事件" + "监听事件" "嘈杂通知" - "响铃通话" + "来电振铃" "静默通知" - "%1$s:%2$d 个消息" + "%1$s:%2$d 条消息" - "%d 个通知" + "%d 条通知" - "UnifiedPush 分发器注册失败,你将无法再接收通知。请检查 app 的通知设置及推送分发器的状态。" - "你有新消息。" + "统一推送通知分发器注册失败,您将无法再接收通知。请检查应用的通知设置及推送分发器的状态。" + "您有新消息。" - "你有 %d 个新消息。" + "您有 %d 条新消息。" "📞 来电" "📹 来电" - "** 无法发送——请打开房间" + "** 无法发送——请打开聊天室" "加入" "拒绝" "%d 个邀请" - "已邀请你聊天" - "%1$s 已邀请你聊天" + "邀请您聊天" + "%1$s 邀您聊天" "提到了你:%1$s" "新消息" - "%d 个新消息" + "%d 条新消息" - "已使用 %1$s 反应" - "设为已读" + "使用 %1$s 回应" + "标记为已读" "快速回复" - "邀请你加入房间" - "%1$s 已邀请你加入房间" + "邀请你加入聊天室" + "%1$s 邀请您加入房间" "我" - "%1$s 个提及或回复" - "已邀请你加入空间" - "%1$s 邀请你加入空间" - "你正在查看通知!点击我!" - "位于 %1$s 中的消息列" + "%1$s提及或回复" + "已邀请您加入该空间" + "%1$s 邀请您加入该空间" + "您正在查看通知!点击我!" + "线程 %1$s" "%1$s:%2$s" "%1$s: %2$s %3$s" - "%d 个未读消息" + "%d 条未读消息" "%1$s 和 %2$s" "%2$s 中的 %1$s" "在 %2$s 和 %3$s 中的 %1$s" - "%d 个房间" + "%d 个聊天室" "后台同步" - "Google 服务" + "谷歌服务" "找不到有效的 Google Play 服务。通知可能无法正常工作。" - "检查被屏蔽的用户" + "检查被阻止的用户" "查看被屏蔽的用户" "未屏蔽任何用户。" - "你已屏蔽 %1$d 位用户。将不再收到这些用户的通知。" + "您已屏蔽 %1$d 位用户。您将不再收到这些用户的推送通知。" "已屏蔽用户" "获取当前推送提供者的名称。" "未选择任何推送提供者。" - "当前推送提供者:%1$s 及当前分发器:%2$s。但未找到分发器 %3$s。该 app 可能已被卸载?" - "当前推送提供者:%1$s ,但尚未配置分发器。" + "当前推送提供商:%1$s和当前分销商:%2$s . 但经销商%3$s未找到。应用程序可能已被卸载?" + "当前推送提供商:%1$s ,但尚未配置分销商。" "当前推送提供者:%1$s。" - "当前推送提供者:%1$s(%2$s)" + "当前推送提供商:%1$s (%2$s )" "当前推送提供者" "确保应用程序至少有一个推送提供者。" "未找到推送提供者。" 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 91f29dd28e..5a0d95c017 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,8 +8,9 @@ package io.element.android.libraries.push.impl.notifications import com.google.common.truth.Truth.assertThat -import io.element.android.features.call.api.CallData +import io.element.android.features.call.api.CallType import io.element.android.features.call.test.FakeElementCallEntryPoint +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 @@ -33,6 +34,7 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore import io.element.android.libraries.push.impl.push.FakeOnNotifiableEventReceived import io.element.android.libraries.push.impl.push.FakeOnRedactedEventReceived +import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.any @@ -104,7 +106,7 @@ class DefaultNotificationResultProcessorTest { @Test fun `when ringing call PushData is received, the incoming call will be handled`() = runTest { val handleIncomingCallLambda = lambdaRecorder< - CallData, + CallType.RoomCall, EventId, UserId, String?, @@ -140,7 +142,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< - CallData, + CallType.RoomCall, EventId, UserId, String?, @@ -176,7 +178,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< - CallData, + CallType.RoomCall, EventId, UserId, String?, @@ -287,6 +289,8 @@ class DefaultNotificationResultProcessorTest { userPushStoreFactory: FakeUserPushStoreFactory = FakeUserPushStoreFactory(), onRedactedEventReceived: (List) -> Unit = {}, onNotifiableEventsReceived: (List) -> Unit = {}, + featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), + syncOnNotifiableEvent: SyncOnNotifiableEvent = {}, elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(), notificationChannels: FakeNotificationChannels = FakeNotificationChannels(), coroutineScope: CoroutineScope = backgroundScope, @@ -297,6 +301,8 @@ class DefaultNotificationResultProcessorTest { userPushStoreFactory = userPushStoreFactory, onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived), onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived), + featureFlagService = featureFlagService, + syncOnNotifiableEvent = syncOnNotifiableEvent, elementCallEntryPoint = elementCallEntryPoint, notificationChannels = notificationChannels, coroutineScope = coroutineScope, 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 deleted file mode 100644 index 63307634f3..0000000000 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultFetchPushForegroundServiceManagerTest.kt +++ /dev/null @@ -1,134 +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.push.impl.push - -import android.app.ActivityManager -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 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 { - @Test - fun `start should start the service if the device is not interactive`() { - val manager = createDefaultFetchPushForegroundServiceManager() - - getShadowPowerManager().turnScreenOn(false) - - assertThat(manager.start()).isTrue() - } - - @Test - fun `start won't start the service if the device is interactive`() { - val manager = createDefaultFetchPushForegroundServiceManager() - - getShadowPowerManager().turnScreenOn(true) - - assertThat(manager.start()).isFalse() - } - - @Test - fun `stop will stop the service if it's running`() = runTest { - val manager = createDefaultFetchPushForegroundServiceManager() - - // Start the service first - getShadowPowerManager().turnScreenOn(false) - manager.start() - - getShadowActivityManager().setServices( - listOf( - ActivityManager.RunningServiceInfo().apply { - service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java) - foreground = true - } - ) - ) - - assertThat(manager.stop()).isTrue() - } - - @Test - fun `stop will eventually stop the service once it's on foreground`() = runTest { - val manager = createDefaultFetchPushForegroundServiceManager() - - // Start the service first - getShadowPowerManager().turnScreenOn(false) - manager.start() - - // The service is started, but not yet in foreground - getShadowActivityManager().setServices( - listOf( - ActivityManager.RunningServiceInfo().apply { - service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java) - foreground = false - } - ) - ) - - // We call stop, which won't stop the service yet since it's not in foreground - val future = async { manager.stop() } - - // Then we set the service as running in foreground, which should allow the stop to complete - getShadowActivityManager().setServices( - listOf( - ActivityManager.RunningServiceInfo().apply { - service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java) - foreground = true - } - ) - ) - - val stopped = withTimeout(5.seconds) { future.await() } - assertThat(stopped).isTrue() - } - - @Test - fun `stop will not stop the service if it's stopped`() = runTest { - val manager = createDefaultFetchPushForegroundServiceManager() - - // Set some fake running service data, even if the service is not really running - getShadowActivityManager().setServices( - listOf( - ActivityManager.RunningServiceInfo().apply { - service = ComponentName(InstrumentationRegistry.getInstrumentation().context, FetchPushForegroundService::class.java) - foreground = true - } - ) - ) - - // Since the service was not really running, it was not stopped - assertThat(manager.stop()).isFalse() - } - - private fun createDefaultFetchPushForegroundServiceManager() = DefaultFetchPushForegroundServiceManager( - context = InstrumentationRegistry.getInstrumentation().context, - ) - - private fun getShadowPowerManager(): ShadowPowerManager { - val powerManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(POWER_SERVICE) as PowerManager - return Shadows.shadowOf(powerManager) - } - - private fun getShadowActivityManager(): ShadowActivityManager { - val activityManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(ACTIVITY_SERVICE) as ActivityManager - return Shadows.shadowOf(activityManager) - } -} 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 f0dee4446c..a16568d400 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -11,7 +11,6 @@ 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 @@ -41,9 +40,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.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 @@ -215,7 +212,7 @@ class DefaultPushHandlerTest { .isCalledOnce() } - private fun TestScope.createDefaultPushHandler( + private fun createDefaultPushHandler( incrementPushCounterResult: () -> Unit = { lambdaError() }, userPushStore: FakeUserPushStore = FakeUserPushStore(), pushClientSecret: PushClientSecret = FakePushClientSecret(), @@ -230,7 +227,6 @@ class DefaultPushHandlerTest { start = {}, stop = {}, ), - dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), ): DefaultPushHandler { return DefaultPushHandler( incrementPushDataStore = object : IncrementPushDataStore { @@ -250,8 +246,7 @@ 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/FetchPendingNotificationWorkerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationWorkerTest.kt index c0fa5d3442..8168019a99 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 @@ -28,7 +28,7 @@ import io.element.android.libraries.push.impl.notifications.FakeNotificationResu import io.element.android.libraries.push.impl.notifications.fixtures.aPushRequest import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.impl.push.SyncOnNotifiableEvent -import io.element.android.libraries.push.test.push.FakeFetchPushForegroundServiceManager +import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory import io.element.android.services.analytics.test.FakeAnalyticsService @@ -239,7 +239,7 @@ class FetchPendingNotificationWorkerTest { pushHistoryService: FakePushHistoryService = FakePushHistoryService(), resultProcessor: FakeNotificationResultProcessor = FakeNotificationResultProcessor(), systemClock: FakeSystemClock = FakeSystemClock(), - pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(), + pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(), ) = FetchPendingNotificationsWorker( params = createWorkerParams(workDataOf("session_id" to input)), context = InstrumentationRegistry.getInstrumentation().context, @@ -250,7 +250,7 @@ class FetchPendingNotificationWorkerTest { pushHistoryService = pushHistoryService, resultProcessor = resultProcessor, systemClock = systemClock, - fetchPushForegroundServiceManager = pushHandlingWakeLock, + pushHandlingWakeLock = pushHandlingWakeLock, ) private fun TestScope.createWorkerParams( 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 a2022ea22d..0c8d870448 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/push/FakeFetchPushForegroundServiceManager.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakeFetchPushForegroundServiceManager.kt deleted file mode 100644 index d0128b4a09..0000000000 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakeFetchPushForegroundServiceManager.kt +++ /dev/null @@ -1,23 +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.push.test.push - -import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager - -class FakeFetchPushForegroundServiceManager( - private val lock: () -> Boolean = { true }, - private val unlock: () -> Boolean = { true }, -) : FetchPushForegroundServiceManager { - override fun start(): Boolean { - return lock.invoke() - } - - override suspend fun stop(): Boolean { - return unlock.invoke() - } -} diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakePushHandlingWakeLock.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakePushHandlingWakeLock.kt new file mode 100644 index 0000000000..077c8f661e --- /dev/null +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/push/FakePushHandlingWakeLock.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.push + +import io.element.android.libraries.push.api.push.PushHandlingWakeLock +import kotlin.time.Duration + +class FakePushHandlingWakeLock( + private val lock: (time: Duration) -> Unit = {}, + private val unlock: () -> Unit = {}, +) : PushHandlingWakeLock { + override fun lock(time: Duration) { + lock.invoke(time) + } + + override suspend fun unlock() { + unlock.invoke() + } +} diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index ffa4e9fa70..49ce7135d5 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -57,7 +57,6 @@ 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/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..3961f1f591 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt @@ -15,7 +15,7 @@ import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.annotations.AppCoroutineScope -import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager +import io.element.android.libraries.push.api.push.PushHandlingWakeLock import io.element.android.libraries.pushproviders.api.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -27,7 +27,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler @Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushHandler: PushHandler - @Inject lateinit var fetchPushForegroundServiceManager: FetchPushForegroundServiceManager + @Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock @AppCoroutineScope @Inject lateinit var coroutineScope: CoroutineScope @@ -49,7 +49,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { val isHighPriority = message.priority == PRIORITY_HIGH if (isHighPriority) { // Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work - fetchPushForegroundServiceManager.start() + pushHandlingWakeLock.lock() } coroutineScope.launch { @@ -63,7 +63,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { }, ) if (isHighPriority) { - fetchPushForegroundServiceManager.stop() + pushHandlingWakeLock.unlock() } } else { val handled = pushHandler.handle( @@ -73,7 +73,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { // If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long. if (!handled && isHighPriority) { - fetchPushForegroundServiceManager.stop() + pushHandlingWakeLock.unlock() } } } diff --git a/libraries/pushproviders/firebase/src/main/res/values-ca/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-ca/translations.xml deleted file mode 100644 index acd61bab06..0000000000 --- a/libraries/pushproviders/firebase/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - "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/VectorFirebaseMessagingServiceTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt index a04c961ebb..798328e626 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt @@ -15,7 +15,7 @@ import com.google.firebase.messaging.RemoteMessage 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_SECRET -import io.element.android.libraries.push.test.push.FakeFetchPushForegroundServiceManager +import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock import io.element.android.libraries.push.test.test.FakePushHandler import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushproviders.api.PushHandler @@ -29,6 +29,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import kotlin.time.Duration @RunWith(RobolectricTestRunner::class) class VectorFirebaseMessagingServiceTest { @@ -80,11 +81,11 @@ class VectorFirebaseMessagingServiceTest { @Test fun `test pushHandler returning true locks and does not unlock the wakelock so it continues running`() = runTest { - val lockLambda = lambdaRecorder { true } - val unlockLambda = lambdaRecorder { true } + val lockLambda = lambdaRecorder { _ -> } + val unlockLambda = lambdaRecorder { } val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( pushHandler = FakePushHandler(handleResult = { _, _ -> true }), - pushHandlingWakeLock = FakeFetchPushForegroundServiceManager( + pushHandlingWakeLock = FakePushHandlingWakeLock( lock = lockLambda, unlock = unlockLambda ) @@ -112,11 +113,11 @@ class VectorFirebaseMessagingServiceTest { @Test fun `test pushHandler returning false locks and unlocks the wakelock early`() = runTest { - val lockLambda = lambdaRecorder { true } - val unlockLambda = lambdaRecorder { true } + val lockLambda = lambdaRecorder { _ -> } + val unlockLambda = lambdaRecorder { } val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( pushHandler = FakePushHandler(handleResult = { _, _ -> false }), - pushHandlingWakeLock = FakeFetchPushForegroundServiceManager( + pushHandlingWakeLock = FakePushHandlingWakeLock( lock = lockLambda, unlock = unlockLambda ) @@ -144,11 +145,11 @@ class VectorFirebaseMessagingServiceTest { @Test fun `test pushHandler with a remote message with normal priority won't lock the wakelock`() = runTest { - val lockLambda = lambdaRecorder { true } - val unlockLambda = lambdaRecorder { true } + val lockLambda = lambdaRecorder { _ -> } + val unlockLambda = lambdaRecorder { } val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( pushHandler = FakePushHandler(handleResult = { _, _ -> false }), - pushHandlingWakeLock = FakeFetchPushForegroundServiceManager( + pushHandlingWakeLock = FakePushHandlingWakeLock( lock = lockLambda, unlock = unlockLambda ) @@ -185,14 +186,14 @@ class VectorFirebaseMessagingServiceTest { private fun TestScope.createVectorFirebaseMessagingService( firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(), pushHandler: PushHandler = FakePushHandler(), - pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(), + pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(), ): VectorFirebaseMessagingService { return VectorFirebaseMessagingService().apply { this.firebaseNewTokenHandler = firebaseNewTokenHandler this.pushParser = FirebasePushParser() this.pushHandler = pushHandler this.coroutineScope = this@createVectorFirebaseMessagingService - this.fetchPushForegroundServiceManager = pushHandlingWakeLock + this.pushHandlingWakeLock = pushHandlingWakeLock } } } 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..363400ba13 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 @@ -14,7 +14,7 @@ import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.annotations.AppCoroutineScope -import io.element.android.libraries.push.api.push.FetchPushForegroundServiceManager +import io.element.android.libraries.push.api.push.PushHandlingWakeLock import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult @@ -38,7 +38,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler @Inject lateinit var removedGatewayHandler: UnifiedPushRemovedGatewayHandler @Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler - @Inject lateinit var fetchPushForegroundServiceManager: FetchPushForegroundServiceManager + @Inject lateinit var pushHandlingWakeLock: PushHandlingWakeLock @AppCoroutineScope @Inject lateinit var coroutineScope: CoroutineScope @@ -59,8 +59,8 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { * @param instance connection, for multi-account */ override fun onMessage(context: Context, message: PushMessage, instance: String) { - // Start the foreground service to ensure the device stays awake while we handle the push and schedule and run the work. - fetchPushForegroundServiceManager.start() + // Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work + pushHandlingWakeLock.lock() Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}") coroutineScope.launch { @@ -71,16 +71,16 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { providerInfo = "${UnifiedPushConfig.NAME} - $instance", data = String(message.content), ) - fetchPushForegroundServiceManager.stop() + pushHandlingWakeLock.unlock() } else { val handled = pushHandler.handle( pushData = pushData, providerInfo = "${UnifiedPushConfig.NAME} - $instance", ) - // If we failed to handle the push, we should stop the foreground service early to avoid keeping the device awake for too long. + // If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long. if (!handled) { - fetchPushForegroundServiceManager.stop() + pushHandlingWakeLock.unlock() } } } diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-ca/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-ca/translations.xml deleted file mode 100644 index 51096a1cfa..0000000000 --- a/libraries/pushproviders/unifiedpush/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - "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/main/res/values-vi/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-vi/translations.xml deleted file mode 100644 index 3305adad2a..0000000000 --- a/libraries/pushproviders/unifiedpush/src/main/res/values-vi/translations.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - "%1$d nhà phân phối đã tìm thấy: %2$s ." - - diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-zh/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-zh/translations.xml index 3b9048a37f..2fac432ca8 100644 --- a/libraries/pushproviders/unifiedpush/src/main/res/values-zh/translations.xml +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-zh/translations.xml @@ -1,9 +1,9 @@ - "确保 UnifiedPush 分发器可用。" - "未找到推送分发器。" + "确保 UnifiedPush distributor 可用。" + "未找到推送 distributor。" - "找到 %1$d 个分发器:%2$s" + "找到 %1$d 个 distributors:%2$s" "检查 UnifiedPush" 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..ef81c647b3 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 @@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SECRET -import io.element.android.libraries.push.test.push.FakeFetchPushForegroundServiceManager +import io.element.android.libraries.push.test.push.FakePushHandlingWakeLock import io.element.android.libraries.push.test.test.FakePushHandler import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushproviders.api.PushHandler @@ -39,6 +39,7 @@ 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 +import kotlin.time.Duration @RunWith(RobolectricTestRunner::class) class VectorUnifiedPushMessagingReceiverTest { @@ -105,13 +106,13 @@ class VectorUnifiedPushMessagingReceiverTest { fun `pushHandler returning true locks the wake lock but does not unlock it so it continues to run`() = runTest { val context = InstrumentationRegistry.getInstrumentation().context val pushHandlerResult = lambdaRecorder { _, _ -> true } - val lockLambda = lambdaRecorder { true } - val unlockLambda = lambdaRecorder { true } + val lockLambda = lambdaRecorder { _ -> } + val unlockLambda = lambdaRecorder { } val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver( pushHandler = FakePushHandler( handleResult = pushHandlerResult ), - pushHandlingWakeLock = FakeFetchPushForegroundServiceManager( + pushHandlingWakeLock = FakePushHandlingWakeLock( lock = lockLambda, unlock = unlockLambda, ), @@ -132,13 +133,13 @@ class VectorUnifiedPushMessagingReceiverTest { fun `pushHandler returning false locks and unlocks the wakelock early`() = runTest { val context = InstrumentationRegistry.getInstrumentation().context val pushHandlerResult = lambdaRecorder { _, _ -> false } - val lockLambda = lambdaRecorder { true } - val unlockLambda = lambdaRecorder { true } + val lockLambda = lambdaRecorder { _ -> } + val unlockLambda = lambdaRecorder { } val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver( pushHandler = FakePushHandler( handleResult = pushHandlerResult ), - pushHandlingWakeLock = FakeFetchPushForegroundServiceManager( + pushHandlingWakeLock = FakePushHandlingWakeLock( lock = lockLambda, unlock = unlockLambda, ), @@ -263,7 +264,7 @@ class VectorUnifiedPushMessagingReceiverTest { unifiedPushNewGatewayHandler: UnifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler(), endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(), removedGatewayHandler: UnifiedPushRemovedGatewayHandler = UnifiedPushRemovedGatewayHandler { lambdaError() }, - pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(), + pushHandlingWakeLock: FakePushHandlingWakeLock = FakePushHandlingWakeLock(), ): VectorUnifiedPushMessagingReceiver { return VectorUnifiedPushMessagingReceiver().apply { this.pushParser = unifiedPushParser @@ -276,7 +277,7 @@ class VectorUnifiedPushMessagingReceiverTest { this.removedGatewayHandler = removedGatewayHandler this.endpointRegistrationHandler = endpointRegistrationHandler this.coroutineScope = this@createVectorUnifiedPushMessagingReceiver - this.fetchPushForegroundServiceManager = pushHandlingWakeLock + this.pushHandlingWakeLock = pushHandlingWakeLock } } } diff --git a/libraries/qrcode/build.gradle.kts b/libraries/qrcode/build.gradle.kts index 1cf6a8cdff..cf76e117c0 100644 --- a/libraries/qrcode/build.gradle.kts +++ b/libraries/qrcode/build.gradle.kts @@ -20,5 +20,4 @@ dependencies { implementation(libs.androidx.camera.camera2) implementation(libs.zxing.cpp) implementation(libs.google.zxing) - implementation(libs.google.guava) } 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 b50b202e2c..e045e42f17 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, - modifier: Modifier = Modifier, forceMaxBrightness: Boolean = true, + modifier: Modifier = Modifier, ) { if (forceMaxBrightness) { ForceMaxBrightness() diff --git a/libraries/recentemojis/impl/build.gradle.kts b/libraries/recentemojis/impl/build.gradle.kts index 061a7ecd89..a1a72c8672 100644 --- a/libraries/recentemojis/impl/build.gradle.kts +++ b/libraries/recentemojis/impl/build.gradle.kts @@ -21,7 +21,6 @@ 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/rustls-tls/README.md b/libraries/rustls-tls/README.md deleted file mode 100644 index aa45fe6743..0000000000 --- a/libraries/rustls-tls/README.md +++ /dev/null @@ -1,9 +0,0 @@ -This module is a wrapper for the Android code distributed in the rustls-platform-verifier-android crate. - -To avoid the distribution mess that this library has (download a Rust crate, then search for it using Gradle and use it as local maven repo), -we previously just manually updated the AAR file instead using a script. This won't work for F-Droid because the AAR library is a black box with -no sources attached to it, so we can't use it like that. - -Instead, for the time being, we're adding the single `CertificateVerifier.kt` class this AAR had in it as part of our sources. - -When this file is updated, the [UPDATED.md](./UPDATED.md) file should be updated too with the commit SHA of the new version. diff --git a/libraries/rustls-tls/UPDATED.md b/libraries/rustls-tls/UPDATED.md deleted file mode 100644 index 10dba2dd53..0000000000 --- a/libraries/rustls-tls/UPDATED.md +++ /dev/null @@ -1,7 +0,0 @@ -Below is the commit SHA in [rustls-platform-verifier](https://github.com/rustls/rustls-platform-verifier) library used to update the code in this module: - -``` -996b1c903491641b17b3c9afb65d1352f6fc6b76 -``` - -Please update it after making manual changes. diff --git a/libraries/rustls-tls/build.gradle.kts b/libraries/rustls-tls/build.gradle.kts deleted file mode 100644 index 85f3f4c476..0000000000 --- a/libraries/rustls-tls/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -import extension.buildConfigFieldBoolean - -/* - * 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 = "org.rustls.platformverifier" - - buildFeatures { - buildConfig = true - } - - defaultConfig { - buildConfigFieldBoolean("TEST", false) - } -} 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 deleted file mode 100644 index 38abed0d92..0000000000 --- a/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt +++ /dev/null @@ -1,480 +0,0 @@ -@file:SuppressLint("LogNotTimber", "ObsoleteSdkInt") -@file:Suppress("KotlinConstantConditions") - -// IMPORTANT: this file comes from rustls-platform-verifier and should not be modified locally. - -/* - * Copyright (c) 2022 1Password - * - * SPDX-License-Identifier: MIT - */ - -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 -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 - -// If this is updated, update the Rust definition too. -// Marked private as this is not meant to be used in Android code. -private enum class StatusCode(val value: Int) { - Ok(0), - Unavailable(1), - Expired(2), - UnknownCert(3), - Revoked(4), - InvalidEncoding(5), - InvalidExtension(6), -} - -// Marked private as this is not meant to be used in Android code. -private class VerificationResult( - status: StatusCode, - @Suppress("unused") val message: String? = null -) { - @Suppress("unused") - private val code: Int = status.value -} - -// NOTE: All TrustManager and certificate validation methods are not thread safe. These -// are all guarded by Kotlin's `Synchronized` accessors to prevent undefined behavior. - -// Only JNI and test code calls this, so unused code warnings are suppressed. -// Internal for test code - no other Kotlin code should use this object directly. -@Suppress("unused") -// We want to show a difference between Kotlin-side logs and those in Rust code -@SuppressLint("LongLogTag") -internal object CertificateVerifier { - private const val TAG = "rustls-platform-verifier-android" - - private fun createTrustManager(keystore: KeyStore?): X509TrustManagerExtensions? { - // This can never throw since the default algorithm is used. - val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - - factory.init(keystore) - - val availableTrustManagers = try { - factory.trustManagers - } catch (e: RuntimeException) { - Log.w(TAG, "exception thrown creating a TrustManager: $e") - return null - } - - for (manager in availableTrustManagers) { - if (manager is X509TrustManager) { - // Kotlin ensures this can't throw at runtime since it knows that - // it must be the correct type by now. - return X509TrustManagerExtensions(manager) - } - } - - Log.e(TAG, "failed to find a usable trust manager") - return null - } - - private fun makeLazyTrustManager(keystore: KeyStore?): Lazy { - // Ensure the keystore is loaded. Since all of the trust managers are initialized in a - // `Lazy`, this will only run once. - keystore?.load(null) - - return lazy { createTrustManager(keystore) } - } - - // -- Test only -- - // Ideally, all of this will be optimized out at compile time due to not being accessed - // in release builds. - - @get:Synchronized - private val mockKeystore: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType()) - - @get:Synchronized - private var mockTrustManager: Lazy = - makeLazyTrustManager(mockKeystore) - - @JvmStatic - private fun addMockRoot(root: ByteArray) { - if (!BuildConfig.TEST) { - throw Exception("attempted to add a mock root outside a test!") - } - - val alias = "root_${mockKeystore.size()}" - // Throwing here is fine since test roots should always be well-formed - val cert = certFactory.generateCertificate(ByteArrayInputStream(root)) - mockKeystore.setCertificateEntry(alias, cert) - - reloadMockData() - } - - @JvmStatic - private fun clearMockRoots() { - // Reload to get a completely fresh internal state - mockKeystore.load(null) - reloadMockData() - } - - @JvmStatic - private fun reloadMockData() { - if (mockTrustManager.isInitialized()) { - mockTrustManager = makeLazyTrustManager(mockKeystore) - } - } - - // Get a list of the system's root CAs. - // Function is public for testing only. - @JvmStatic - fun getSystemRootCAs(): List { - val rootCAs = mutableListOf() - - val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - factory.init(systemKeystore) - - val availableTrustManagers = try { - factory.trustManagers - } catch (e: RuntimeException) { - Log.w(TAG, "exception thrown creating a TrustManager: $e") - return rootCAs - } - - availableTrustManagers.forEach { trustManager -> - if (trustManager is X509TrustManager) { - rootCAs.addAll(trustManager.acceptedIssuers) - } - } - - return rootCAs - } - - // -- End testing requirements -- - - private val certFactory: CertificateFactory = CertificateFactory.getInstance("X.509") - - private var systemTrustAnchorCache = hashSetOf>() - - @get:Synchronized - private var systemCertificateDirectory: File? = System.getenv("ANDROID_ROOT")?.let { rootPath -> - File("$rootPath/etc/security/cacerts") - } - - @get:Synchronized - private val systemKeystore: KeyStore? = try { - KeyStore.getInstance("AndroidCAStore") - } catch (_: KeyStoreException) { - null - } - - @get:Synchronized - private val systemTrustManager: Lazy = - makeLazyTrustManager(systemKeystore) - - @JvmStatic - private fun verifyCertificateChain( - @Suppress("UNUSED_PARAMETER") context: Context, - serverName: String, - authMethod: String, - allowedEkus: Array, - ocspResponse: ByteArray?, - time: Long, - certChain: Array - ): VerificationResult { - // Convert the array of (supposedly) DER bytes into certificates. - val certificateChain = mutableListOf() - certChain.forEach { certBytes -> - val certificate = try { - certFactory.generateCertificate(ByteArrayInputStream(certBytes)) - } catch (e: CertificateException) { - return VerificationResult(StatusCode.InvalidEncoding) - } - certificateChain.add(certificate as X509Certificate) - } - - // Will never throw `ArrayIndexOutOfBoundsException` because `rustls`'s `ServerCertVerifier` trait - // has a mandatory `end_entity` parameter in `verify_server_cert`. - val endEntity = certificateChain[0] - - // Check that the certificate is valid at the point of time provided by `rustls`. - try { - endEntity.checkValidity(Date(time)) - } catch (e: CertificateExpiredException) { - return VerificationResult(StatusCode.Expired) - } catch (e: CertificateNotYetValidException) { - return VerificationResult(StatusCode.Expired) - } - - // Check that this certificate can be used in a TLS server. - if (!verifyCertUsage(endEntity, allowedEkus)) { - return VerificationResult(StatusCode.InvalidExtension) - } - - // Select the trust manager to use. - // - // We select them as follows: - // - If built for release, only use the system trust manager. This should let all test-related - // code be optimized out. - // - If built for tests: - // - If the mock CA store has any values, use the mock trust manager. - // - Otherwise, use the system trust manager. - val (trustManager, keystore) = if (!BuildConfig.TEST) { - val trustManager = - systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable) - Pair(trustManager, systemKeystore) - } else { - if (mockKeystore.size() != 0) { - val trustManager = mockTrustManager.value!! - Pair(trustManager, mockKeystore) - } else { - val trustManager = - systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable) - Pair(trustManager, systemKeystore) - } - } - - // Verify that the certificate chain is valid and correct, and nothing more. - // - // NOTE: This does not validate `serverName` is valid for the end-entity certificate. - // That is handled in Rust as Android/Java do not currently provide a RFC 6125 compliant - // hostname verifier. Additionally, even the RFC 2818 verifier is not available until API 24. - // - // `serverName` is only used for pinning/CT requirements. - // - // Returns the "the properly ordered chain used for verification as a list of X509Certificates.", - // meaning a list from end-entity certificate to trust-anchor. - val validChain = try { - trustManager.checkServerTrusted(certificateChain.toTypedArray(), authMethod, serverName) - } catch (e: CertificateException) { - // In test configurations we may see `checkServerTrusted` fail once vendored test - // certificates pass their expiry date. We try to avoid that by using a fixed - // verification time when calling `endEntity.checkValidity` above, however we can't - // fix the time for the `checkServerTrusted` call. - // - // To make diagnosing CI test failures easier we try to find the root cause of - // checkServerTrusted failing, returning a different `StatusCode` as appropriate. - if (BuildConfig.TEST) { - var rootCause: Throwable? = e - while (rootCause?.cause != null && rootCause.cause != rootCause) { - rootCause = rootCause.cause - } - return when (rootCause) { - is CertificateExpiredException, is CertificateNotYetValidException -> VerificationResult( - StatusCode.Expired, - rootCause.toString() - ) - - else -> VerificationResult(StatusCode.UnknownCert, rootCause.toString()) - } - } - // In non-test configurations we should have caught expiry errors earlier and - // can simply return an unknown cert error without digging through the exception - // cause chain. - return VerificationResult(StatusCode.UnknownCert, e.toString()) - } - - // TEST ONLY: Mock test suite cannot attempt to check revocation status if no OSCP data has been stapled, - // because Android requires certificates to an specify OCSP responder for network fetch in this case. - // If in testing w/o OCSP stapled, short-circuit here - only prior checks apply. - if (BuildConfig.TEST && (mockKeystore.size() != 0) && (ocspResponse == null)) { - return VerificationResult(StatusCode.Ok) - } - - // Try to check the revocation status of the cert, if it is supported. - // - // 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") - } - - return VerificationResult(StatusCode.Ok) - } - - private fun verifyCertUsage(certificate: X509Certificate, allowedEkus: Array): Boolean { - val ekus = try { - certificate.extendedKeyUsage - } - // This should be unreachable, but could happen. - catch (_: CertificateParsingException) { - return false - } catch (_: NullPointerException) { - // According to Chromium's implementation, this can crash when the EKU data is malformed. - Log.w(TAG, "exception handling certificate EKU") - return false - } ?: return true // If the list is empty, we have nothing to do. - - return ekus.any { allowedEkus.contains(it) } - } - - // Android hashes a principal using the first four bytes of its MD5 digest, encoded in - // lowercase hex and reversed. - // - // Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=339 - private fun hashPrincipal(principal: X500Principal): String { - val hexDigits = "0123456789abcdef".toCharArray() - val digest = MessageDigest.getInstance("MD5").digest(principal.encoded) - val hexChars = CharArray(8) - - for (i in 0..3) { - // Kotlin doesn't support bitwise operators for bytes, only Int and Long. - val digestByte = digest[3 - i].toInt() - hexChars[2 * i] = hexDigits[(digestByte shr 4) and 0xf] - hexChars[2 * i + 1] = hexDigits[digestByte and 0xf] - } - - return String(hexChars) - } - - // Check if CA root is known or not. - // Known means installed in root CA store, either a preset public CA or a custom one installed by an enterprise/user. - // - // Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=351 - fun isKnownRoot(root: X509Certificate): Boolean { - // System keystore and cert directory must be non-null to perform checking - systemKeystore?.let { loadedSystemKeystore -> - systemCertificateDirectory?.let { loadedSystemCertificateDirectory -> - - // Check the in-memory cache first - val key = Pair(root.subjectX500Principal, root.publicKey) - if (systemTrustAnchorCache.contains(key)) { - return true - } - - // System trust anchors are stored under a hash of the principal. - // In case of collisions, append number. - val hash = hashPrincipal(root.subjectX500Principal) - var i = 0 - while (true) { - val alias = "$hash.$i" - - if (!File(loadedSystemCertificateDirectory, alias).exists()) { - break - } - - val anchor = loadedSystemKeystore.getCertificate("system:$alias") - - // It's possible for `anchor` to be `null` if the user deleted a trust anchor. - // Continue iterating as there may be further collisions after the deleted anchor. - if (anchor == null) { - continue - // This should never happen - } else if (anchor !is X509Certificate) { - // SAFETY: This logs a unique identifier (hash value) only in cases where a file within the - // system's root trust store is not a valid X509 certificate (extremely unlikely error). - // The hash doesn't tell us any sensitive information about the invalid cert or reveal any of - // its contents - it just lets us ID the bad file if a user is having TLS failure issues. - Log.e(TAG, "anchor is not a certificate, alias: $alias") - continue - // If subject and public key match, it's a system root. - } else { - if ((root.subjectX500Principal == anchor.subjectX500Principal) && (root.publicKey == anchor.publicKey)) { - systemTrustAnchorCache.add(key) - return true - } - } - - i += 1 - } - } - } - - // Not found in cache or store: non-public - return false - } -} 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 88cf9558b3..568dbe7e3a 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 Authorization info for this session, if any. */ - val oAuthData: String?, + /** The Open ID Connect info for this session, if any. */ + val oidcData: 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 316bf86c1c..ea69709bbd 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 = oAuthData, + oidcData = oidcData, 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, - oAuthData = oidcData, + oidcData = oidcData, loginTimestamp = loginTimestamp?.let { Date(it) }, isTokenValid = isTokenValid == 1L, loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name), diff --git a/libraries/session-storage/test/build.gradle.kts b/libraries/session-storage/test/build.gradle.kts index 7a89746812..cfdc3018a9 100644 --- a/libraries/session-storage/test/build.gradle.kts +++ b/libraries/session-storage/test/build.gradle.kts @@ -14,7 +14,6 @@ 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 c5acd77755..c791a20620 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", - oAuthData = null, + oidcData = 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 a7c3a6837b..fdf5cc5f1b 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,13 +10,12 @@ 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 = CopyOnWriteArraySet() + private val _listeners = mutableListOf() val listeners: List - get() = _listeners.toList() + get() = _listeners override fun addListener(listener: SessionListener) { _listeners.add(listener) 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 4b3a0d6ffa..e564ddac41 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 @@ -124,6 +124,4 @@ 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/CaptionWarningBottomSheet.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/CaptionWarningBottomSheet.kt index bec837664f..23b4fae3a9 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/CaptionWarningBottomSheet.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/CaptionWarningBottomSheet.kt @@ -12,8 +12,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -39,13 +37,11 @@ fun CaptionWarningBottomSheet( ModalBottomSheet( modifier = modifier, onDismissRequest = onDismiss, - scrollable = false, ) { Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()), + .padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), ) { diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt index 039f516547..08d89e25fb 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt @@ -24,7 +24,6 @@ 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.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter @@ -65,7 +64,7 @@ internal fun ComposerModeView( } is MessageComposerMode.Reply -> { ReplyToModeView( - modifier = modifier.padding(top = 8.dp, start = 8.dp, end = 8.dp), + modifier = modifier.padding(8.dp), replyToDetails = composerMode.replyToDetails, hideImage = composerMode.hideImage, onResetComposerMode = onResetComposerMode, @@ -121,9 +120,6 @@ private fun EditingModeView( } } -// This combination of density DPI and font scale is an approximation to a screen with little space to display the content -private const val MAX_SCALING_VALUE = 3.5f - /** * https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2019-6286 */ @@ -141,14 +137,9 @@ private fun ReplyToModeView( .border(1.dp, ElementTheme.colors.separatorPrimary, RoundedCornerShape(6.dp)) .padding(4.dp) ) { - // Larger density DPI and font scale means less space to display the content, so we limit it to 1 line to avoid overflow issues - val currentDensity = LocalDensity.current - val hasLowResolution = currentDensity.density * currentDensity.fontScale >= MAX_SCALING_VALUE - val maxReplyContentLines = if (hasLowResolution) 1 else 2 InReplyToView( inReplyTo = replyToDetails, hideImage = hideImage, - maxLines = maxReplyContentLines, modifier = Modifier.weight(1f), ) Icon( 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 3993b3d64c..bdaed4e402 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 @@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -41,7 +40,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource @@ -52,7 +50,6 @@ import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -69,14 +66,10 @@ import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconColorButton import io.element.android.libraries.designsystem.theme.components.Text 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.timeline.item.event.EventOrTransactionId -import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent -import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider -import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.textcomposer.components.SendButtonIcon @@ -188,7 +181,7 @@ fun TextComposer( placeholder = placeholder, registerStateUpdates = true, modifier = Modifier - .padding(top = 4.dp, bottom = 6.dp) + .padding(top = 6.dp, bottom = 6.dp) .fillMaxWidth(), style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.richTextEditorState.hasFocus), resolveMentionDisplay = resolveMentionDisplay, @@ -407,7 +400,6 @@ fun TextComposer( onAddAttachment = onAddAttachment, onDeleteVoiceMessage = onDeleteVoiceMessage, onVoiceRecorderEvent = onVoiceRecorderEvent, - onResetComposerMode = onResetComposerMode, ) } @@ -417,15 +409,6 @@ fun TextComposer( SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it } - // Re-focus the text input when voice recording ends so the user can continue typing - var previousVoiceMessageState by remember { mutableStateOf(voiceMessageState) } - LaunchedEffect(voiceMessageState) { - if (voiceMessageState is VoiceMessageState.Idle && previousVoiceMessageState !is VoiceMessageState.Idle) { - onRequestFocus() - } - previousVoiceMessageState = voiceMessageState - } - val latestOnReceiveSuggestion by rememberUpdatedState(onReceiveSuggestion) if (state is TextEditorState.Rich) { val menuAction = state.richTextEditorState.menuAction @@ -457,7 +440,6 @@ private fun StandardLayout( onAddAttachment: () -> Unit, onDeleteVoiceMessage: () -> Unit, onVoiceRecorderEvent: (VoiceMessageRecorderEvent) -> Unit, - onResetComposerMode: () -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -524,14 +506,6 @@ private fun StandardLayout( ) { if (voiceMessageState is VoiceMessageState.Idle) { textInput() - } else if (composerMode is MessageComposerMode.Special) { - TextInputBox( - composerMode = composerMode, - onResetComposerMode = onResetComposerMode, - isTextEmpty = true, - ) { - voiceRecording() - } } else { voiceRecording() } @@ -658,14 +632,10 @@ private fun TextInputBox( composerMode = composerMode, onResetComposerMode = onResetComposerMode, ) - } else { - // Top padding for the message composer box - Spacer(Modifier.height(4.dp)) } - Box( modifier = Modifier - .padding(top = 1.dp, bottom = 4.dp, start = 12.dp, end = 12.dp) + .padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp) .then(Modifier.testTag(TestTags.textEditor)), contentAlignment = Alignment.CenterStart, ) { @@ -675,11 +645,11 @@ private fun TextInputBox( Icon( modifier = Modifier .clickable { showBottomSheet = true } - .padding(start = 8.dp, end = 8.dp, top = 4.dp, bottom = 4.dp) + .padding(horizontal = 8.dp, vertical = 4.dp) .align(Alignment.CenterEnd), imageVector = CompoundIcons.InfoSolid(), tint = ElementTheme.colors.iconCriticalPrimary, - contentDescription = stringResource(CommonStrings.a11y_info), + contentDescription = null, ) if (showBottomSheet) { CaptionWarningBottomSheet( @@ -994,40 +964,6 @@ internal fun TextComposerVoiceNotEncryptedPreview() = ElementPreview { } } -@Preview -@Composable -internal fun TextComposerScaledDensityWithReplyPreview() { - ElementPreview { - CompositionLocalProvider( - LocalDensity provides Density( - density = 3f, - fontScale = 1.25f, - ), - ) { - val replyToDetails = InReplyToDetails.Ready( - eventId = EventId("\$1234"), - senderId = UserId("@alice:example.com"), - senderProfile = aProfileDetailsReady(), - eventContent = MessageContent( - body = "Message which are being replied, and which was long enough to be displayed on two lines (only!).", - inReplyTo = null, - isEdited = false, - threadInfo = null, - type = TextMessageType("Message which are being replied, and which was long enough to be displayed on two lines (only!).", null) - ), - textContent = "Message which are being replied, and which was long enough to be displayed on two lines (only!).", - ) - Box(modifier = Modifier.width(480.dp).height(120.dp)) { - ATextComposer( - state = aTextEditorStateMarkdown(initialText = "", initialFocus = true), - voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Reply(replyToDetails, hideImage = false), - ) - } - } - } -} - @Composable private fun PreviewColumn( items: ImmutableList, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt index bd6944d603..b3c60b69d7 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt @@ -82,7 +82,7 @@ fun MarkdownTextInput( AndroidView( modifier = Modifier - .padding(top = 5.dp, bottom = 6.dp) + .padding(top = 6.dp, bottom = 6.dp) .fillMaxWidth(), factory = { context -> MarkdownEditText(context).apply { diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpan.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpan.kt index e2ae08c421..b0f973b6b9 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpan.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpan.kt @@ -105,12 +105,15 @@ class MentionSpan( bottom: Int, paint: Paint ) { + // Extra vertical space to add below the baseline (y). This helps us center the span vertically + val extraVerticalSpace = y + paint.ascent() + paint.descent() - top + val availableWidth = (canvas.width - x).coerceAtLeast(0f) val measuredWidth = measuredTextWidth + startPadding + endPadding val pillWidth = minOf(availableWidth, measuredWidth.toFloat()) backgroundPaint.color = backgroundColor - val rect = RectF(x, top.toFloat(), x + pillWidth, bottom.toFloat()) + val rect = RectF(x, top.toFloat(), x + pillWidth, y.toFloat() + extraVerticalSpace) val radius = rect.height() / 2 canvas.drawRoundRect(rect, radius, radius, backgroundPaint) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt index 90e5368951..6560bd8b69 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt @@ -85,6 +85,15 @@ class MarkdownTextEditorState( val length = resolvedSuggestion.command.command.length + 1 selection = IntRange(length, length) } + is ResolvedSuggestion.Command -> { + // Insert the command text with a trailing space + val commandWithSpace = "${resolvedSuggestion.command} " + val currentText = SpannableStringBuilder(text.value()) + currentText.replace(suggestion.start, suggestion.end, commandWithSpace) + val newCursorPosition = suggestion.start + commandWithSpace.length + this.text.update(currentText, true) + this.selection = IntRange(newCursorPosition, newCursorPosition) + } } } diff --git a/libraries/textcomposer/impl/src/main/res/values-ca/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index f3b78b9dd2..0000000000 --- a/libraries/textcomposer/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - "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-vi/translations.xml b/libraries/textcomposer/impl/src/main/res/values-vi/translations.xml index 5395221a6d..42b78d23f3 100644 --- a/libraries/textcomposer/impl/src/main/res/values-vi/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-vi/translations.xml @@ -4,18 +4,11 @@ "Chuyển đổi danh sách dấu đầu dòng" "Hủy và đóng định dạng văn bản" "Bật/tắt khối mã" - "Thêm chú thích" - "Tin nhắn được mã hóa…" "Tin nhắn…" - "Tin nhắn chưa được mã hóa…" "Tạo liên kết" "Sửa liên kết" - "%1$s, tình trạng: %2$s" "Áp dụng định dạng in đậm" "Áp dụng định dạng in nghiêng" - "Đã tắt" - "tắt" - "bật" "Áp dụng định dạng gạch ngang" "Áp dụng định dạng gạch chân" "Bật/tắt chế độ toàn màn hình" 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 f79cb2f30c..8db2b9c767 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 @@ "切换符号列表" "取消并关闭文本格式" "切换代码块" - "添加标题…" - "已加密的消息…" + "可选的标题……" + "加密信息…" "消息…" "未加密的消息…" "创建链接" @@ -20,7 +20,7 @@ "应用下划线格式" "切换全屏模式" "缩进" - "应用内联代码格式" + "应用行内代码格式" "设置链接" "切换编号列表" "打开撰写选项" 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 4840569c0e..9a65ca0ad5 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,15 +6,12 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.core.text.getSpans import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -35,54 +32,66 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MarkdownTextInputTest { + @get:Rule val rule = createAndroidComposeRule() + @Test - fun `when user types onTyping is triggered with value 'true'`() = runAndroidComposeUiTest { + fun `when user types onTyping is triggered with value 'true'`() = runTest { val state = aMarkdownTextEditorState(initialFocus = true) val onTyping = EnsureCalledOnceWithParam(expectedParam = true, result = Unit) - setMarkdownTextInput(state = state, onTyping = onTyping) - activity!!.findEditor().setText("Test") - awaitIdle() + rule.setMarkdownTextInput(state = state, onTyping = onTyping) + rule.activityRule.scenario.onActivity { + it.findEditor().setText("Test") + } + rule.awaitIdle() onTyping.assertSuccess() } @Test - fun `when user removes text onTyping is triggered with value 'false'`() = runAndroidComposeUiTest { + fun `when user removes text onTyping is triggered with value 'false'`() = runTest { val state = aMarkdownTextEditorState(initialFocus = true) val onTyping = EventsRecorder() - setMarkdownTextInput(state = state, onTyping = onTyping) - val editText = activity!!.findEditor() - editText.setText("Test") - editText.setText("") - editText.setText(null) - awaitIdle() + rule.setMarkdownTextInput(state = state, onTyping = onTyping) + rule.activityRule.scenario.onActivity { + val editText = it.findEditor() + editText.setText("Test") + editText.setText("") + editText.setText(null) + } + rule.awaitIdle() onTyping.assertList(listOf(true, false, false)) } @Test - fun `when user types something that's not a mention onSuggestionReceived is triggered with 'null'`() = runAndroidComposeUiTest { + fun `when user types something that's not a mention onSuggestionReceived is triggered with 'null'`() = runTest { val state = aMarkdownTextEditorState(initialFocus = true) val onSuggestionReceived = EventsRecorder() - setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) - activity!!.findEditor().setText("Test") - awaitIdle() + rule.setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) + rule.activityRule.scenario.onActivity { + it.findEditor().setText("Test") + } + rule.awaitIdle() onSuggestionReceived.assertSingle(null) } @Test - fun `when user types something that's a mention onSuggestionReceived is triggered a real value`() = runAndroidComposeUiTest { + fun `when user types something that's a mention onSuggestionReceived is triggered a real value`() = runTest { val state = aMarkdownTextEditorState(initialFocus = true) val onSuggestionReceived = EventsRecorder() - setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) - val editor = activity!!.findEditor() - editor.setText("@") - editor.setText("#") - editor.setText("/") - awaitIdle() + rule.setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) + rule.activityRule.scenario.onActivity { + it.findEditor().setText("@") + it.findEditor().setText("#") + it.findEditor().setText("/") + } + rule.awaitIdle() onSuggestionReceived.assertList( listOf( // User mention suggestion @@ -96,59 +105,69 @@ class MarkdownTextInputTest { } @Test - fun `when the selection changes in the UI the state is updated`() = runAndroidComposeUiTest { + fun `when the selection changes in the UI the state is updated`() = runTest { val state = aMarkdownTextEditorState(initialText = "Test", initialFocus = true) - setMarkdownTextInput(state = state) - val editor = activity!!.findEditor() - editor.setSelection(2) - awaitIdle() + rule.setMarkdownTextInput(state = state) + rule.activityRule.scenario.onActivity { + val editor = it.findEditor() + editor.setSelection(2) + } + rule.awaitIdle() // Selection is updated assertThat(state.selection).isEqualTo(2..2) } @Test - fun `when the selection state changes in the view is updated`() = runAndroidComposeUiTest { + fun `when the selection state changes in the view is updated`() = runTest { val state = aMarkdownTextEditorState(initialText = "Test", initialFocus = true) - setMarkdownTextInput(state = state) - val editor = activity!!.findEditor() - state.selection = 2..2 - awaitIdle() + rule.setMarkdownTextInput(state = state) + var editor: EditText? = null + rule.activityRule.scenario.onActivity { + editor = it.findEditor() + state.selection = 2..2 + } + rule.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`() = runAndroidComposeUiTest { + fun `when the view focus changes the state is updated`() = runTest { val state = aMarkdownTextEditorState(initialText = "Test", initialFocus = false) - setMarkdownTextInput(state = state) - val editor = activity!!.findEditor() - editor.requestFocus() + rule.setMarkdownTextInput(state = state) + rule.activityRule.scenario.onActivity { + val editor = it.findEditor() + editor.requestFocus() + } // Focus state is updated assertThat(state.hasFocus).isTrue() } @Test - fun `inserting a mention replaces the existing text with a span`() = runAndroidComposeUiTest { + fun `inserting a mention replaces the existing text with a span`() = runTest { val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(A_SESSION_ID) }) val state = aMarkdownTextEditorState(initialText = "@", initialFocus = true) state.currentSuggestion = Suggestion(0, 1, SuggestionType.Mention, "") - setMarkdownTextInput(state = state) - val editor = activity!!.findEditor() - state.insertSuggestion( - ResolvedSuggestion.Member(roomMember = aRoomMember()), - aMentionSpanProvider(permalinkParser), - ) - awaitIdle() + 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() // 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 AndroidComposeUiTest.setMarkdownTextInput( + private fun AndroidComposeTestRule.setMarkdownTextInput( state: MarkdownTextEditorState = aMarkdownTextEditorState(), onTyping: (Boolean) -> Unit = {}, onSuggestionReceived: (Suggestion?) -> Unit = {}, diff --git a/libraries/troubleshoot/impl/src/main/res/values-ca/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ca/translations.xml deleted file mode 100644 index fe85dadb55..0000000000 --- a/libraries/troubleshoot/impl/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - "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-zh/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-zh/translations.xml index e451d82a9b..2375580f2c 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-zh/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-zh/translations.xml @@ -1,10 +1,10 @@ - "推送历史" + "推送历史记录" "运行测试" "再次运行测试" "一些测试失败了。请查看详情。" - "运行测试以检测配置中可能导致通知行为异常的问题。" + "运行测试以检测您的配置中可能导致通知无法按预期运行的问题。" "尝试修复" "所有测试均成功通过。" "排查通知问题" 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 0244673ea5..0ba6c22710 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,58 +6,60 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +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.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 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 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() + @Test - fun `press menu back invokes the expected callback`() = runAndroidComposeUiTest { + fun `press menu back invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - setTroubleshootNotificationsView( + rule.setTroubleshootNotificationsView( state = aTroubleshootNotificationsState( eventSink = eventsRecorder ), onBackClick = it, ) - pressBack() + rule.pressBack() } } @Test - fun `clicking on run test emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on run test emits the expected Event`() { val eventsRecorder = EventsRecorder() - setTroubleshootNotificationsView( + rule.setTroubleshootNotificationsView( aTroubleshootNotificationsState( eventSink = eventsRecorder ), ) - onNodeWithText("Run tests").performClick() + rule.onNodeWithText("Run tests").performClick() eventsRecorder.assertSingle(TroubleshootNotificationsEvents.StartTests) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on run test again emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on run test again emits the expected Event`() { val eventsRecorder = EventsRecorder() - setTroubleshootNotificationsView( + rule.setTroubleshootNotificationsView( aTroubleshootNotificationsState( tests = listOf( aTroubleshootTestStateFailure( @@ -67,7 +69,7 @@ class TroubleshootNotificationsViewTest { eventSink = eventsRecorder ), ) - onNodeWithText("Run tests again").performClick() + rule.onNodeWithText("Run tests again").performClick() eventsRecorder.assertList( listOf( TroubleshootNotificationsEvents.RetryFailedTests, @@ -78,9 +80,9 @@ class TroubleshootNotificationsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on quick fix emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on quick fix emits the expected Event`() { val eventsRecorder = EventsRecorder() - setTroubleshootNotificationsView( + rule.setTroubleshootNotificationsView( aTroubleshootNotificationsState( tests = listOf( aTroubleshootTestStateFailure( @@ -90,7 +92,7 @@ class TroubleshootNotificationsViewTest { eventSink = eventsRecorder ), ) - onNodeWithText("Attempt to fix").performClick() + rule.onNodeWithText("Attempt to fix").performClick() eventsRecorder.assertList( listOf( TroubleshootNotificationsEvents.RetryFailedTests, @@ -100,7 +102,7 @@ class TroubleshootNotificationsViewTest { } } -private fun AndroidComposeUiTest.setTroubleshootNotificationsView( +private fun AndroidComposeTestRule.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 94cde37452..fa4e65ad9a 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,17 +6,14 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_FORMATTED_DATE @@ -26,62 +23,67 @@ 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 org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PushHistoryViewTest { + @get:Rule + val rule = createAndroidComposeRule() + @Test - fun `clicking on Reset sends a PushHistoryEvents`() = runAndroidComposeUiTest { + fun `clicking on Reset sends a PushHistoryEvents`() { val eventsRecorder = EventsRecorder() - setPushHistoryView( + rule.setPushHistoryView( aPushHistoryState( pushCounter = 123, eventSink = eventsRecorder, ), ) - val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) - onNodeWithContentDescription(menuContentDescription).performClick() - clickOn(CommonStrings.action_reset) + val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) + rule.onNodeWithContentDescription(menuContentDescription).performClick() + rule.clickOn(CommonStrings.action_reset) eventsRecorder.assertSingle(PushHistoryEvents.Reset(requiresConfirmation = true)) // Also check that the push counter is rendered - onNodeWithText("123").assertExists() + rule.onNodeWithText("123").assertExists() } @Test - fun `clicking on show only errors sends a PushHistoryEvents(true)`() = runAndroidComposeUiTest { + fun `clicking on show only errors sends a PushHistoryEvents(true)`() { val eventsRecorder = EventsRecorder() - setPushHistoryView( + rule.setPushHistoryView( aPushHistoryState( showOnlyErrors = false, eventSink = eventsRecorder, ), ) - val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) - onNodeWithContentDescription(menuContentDescription).performClick() - onNodeWithText("Show only errors").performClick() + val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) + rule.onNodeWithContentDescription(menuContentDescription).performClick() + rule.onNodeWithText("Show only errors").performClick() eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = true)) } @Test - fun `clicking on show only errors sends a PushHistoryEvents(false)`() = runAndroidComposeUiTest { + fun `clicking on show only errors sends a PushHistoryEvents(false)`() { val eventsRecorder = EventsRecorder() - setPushHistoryView( + rule.setPushHistoryView( aPushHistoryState( showOnlyErrors = true, eventSink = eventsRecorder, ), ) - val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) - onNodeWithContentDescription(menuContentDescription).performClick() - onNodeWithText("Show only errors").performClick() + val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) + rule.onNodeWithContentDescription(menuContentDescription).performClick() + rule.onNodeWithText("Show only errors").performClick() eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = false)) } @Test - fun `clicking on an invalid event has no effect`() = runAndroidComposeUiTest { + fun `clicking on an invalid event has no effect`() { val eventsRecorder = EventsRecorder(expectEvents = false) - setPushHistoryView( + rule.setPushHistoryView( aPushHistoryState( pushHistoryItems = listOf( aPushHistoryItem( @@ -91,14 +93,14 @@ class PushHistoryViewTest { eventSink = eventsRecorder, ), ) - onNodeWithText(A_FORMATTED_DATE).performClick() + rule.onNodeWithText(A_FORMATTED_DATE).performClick() // No callback invoked } @Test - fun `clicking on a valid event emits the expected Event`() = runAndroidComposeUiTest { + fun `clicking on a valid event emits the expected Event`() { val eventsRecorder = EventsRecorder() - setPushHistoryView( + rule.setPushHistoryView( aPushHistoryState( pushHistoryItems = listOf( aPushHistoryItem( @@ -111,7 +113,7 @@ class PushHistoryViewTest { eventSink = eventsRecorder, ), ) - onNodeWithText(A_FORMATTED_DATE).performClick() + rule.onNodeWithText(A_FORMATTED_DATE).performClick() eventsRecorder.assertSingle( PushHistoryEvents.NavigateTo( sessionId = A_SESSION_ID, @@ -122,7 +124,7 @@ class PushHistoryViewTest { } } -private fun AndroidComposeUiTest.setPushHistoryView( +private fun AndroidComposeTestRule.setPushHistoryView( state: PushHistoryState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/libraries/ui-strings/src/main/kotlin/io/element/android/libraries/ui/strings/Strings.kt b/libraries/ui-strings/src/main/kotlin/io/element/android/libraries/ui/strings/Strings.kt deleted file mode 100644 index c4566a5db2..0000000000 --- a/libraries/ui-strings/src/main/kotlin/io/element/android/libraries/ui/strings/Strings.kt +++ /dev/null @@ -1,12 +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.ui.strings - -object Strings { - const val NICE_SEPARATOR = " • " -} 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 f550ce1075..4011474193 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -170,6 +170,7 @@ "Аўтарскае права" "Стварэнне пакоя…" "Выйшаў з пакоя" + "Цёмная" "Памылка расшыфроўкі" "Параметры распрацоўшчыка" "Прамы чат" @@ -196,6 +197,7 @@ "Усталяваць APK" "Гэты Matrix ID не знойдзены, таму запрашэнне можа быць не атрымана." "Пакінуць пакой" + "Светлая" "Спасылка скапіравана ў буфер абмену" "Загрузка…" @@ -242,6 +244,7 @@ "Ключ аднаўлення" "Абнаўленне…" "Адказвае на %1$s" + "Паведаміць пра памылку" "Паведаміць аб праблеме" "Скарга прынята" "Рэдактар фарматаванага тэксту" @@ -270,6 +273,7 @@ "Поспех" "Прапановы" "Сінхранізацыя" + "Сістэмная" "Тэкст" "Паведамленні трэціх асоб" "Гутарка" 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 3f38b1991f..94e050727d 100644 --- a/libraries/ui-strings/src/main/res/values-bg/translations.xml +++ b/libraries/ui-strings/src/main/res/values-bg/translations.xml @@ -145,6 +145,7 @@ "Създаване на стая…" "Стаята е напусната" "Пространството е напуснато" + "Тъмен" "Грешка при разшифроване" "Описание" "Опции за разработчици" @@ -175,6 +176,7 @@ "Този Matrix ID не може да бъде намерен, така че поканата може да не бъде получена." "Стаята се напуска" "Пространството се напуска" + "Светъл" "Връзката е копирана в клипборда" "Зарежда се…" "Зарежда се още…" @@ -229,6 +231,7 @@ "%1$d отговора" "В отговор на %1$s" + "Съобщаване за грешка" "Съобщаване за проблем" "Докладът е изпратен" "Редактор на богат текст" @@ -267,6 +270,7 @@ "Успешно" "Предложения" "Синхронизиране" + "Система" "Текст" "Уведомления от трети страни" "Нишка" diff --git a/libraries/ui-strings/src/main/res/values-ca/translations.xml b/libraries/ui-strings/src/main/res/values-ca/translations.xml deleted file mode 100644 index d8abb7ff73..0000000000 --- a/libraries/ui-strings/src/main/res/values-ca/translations.xml +++ /dev/null @@ -1,387 +0,0 @@ - - - "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 1bf3802b12..38d9ac3d8e 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -15,7 +15,6 @@ "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" @@ -51,10 +50,8 @@ "Odeslat soubory" "Poloha odesílatele" "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" "Místnost s náhrobkem" "Avatar uživatele" @@ -73,7 +70,6 @@ "Hovor" "Zrušit" "Prozatím zrušit" - "Vyberte soubor" "Vybrat fotku" "Vymazat" "Zavřít" @@ -93,15 +89,12 @@ "Deaktivovat účet" "Odmítnout" "Odmítnout a zablokovat" - "Smazat" - "Smazat účet" "Odstranit hlasování" "Odznačit vše" "Zakázat" "Vyřadit" "Zavřít" "Hotovo" - "Stáhnout" "Upravit" "Upravit titulek" "Upravit hlasování" @@ -201,6 +194,7 @@ "Pokročilá nastavení" "obrázek" "Analytika" + "Synchronizace oznámení…" "Opustili jste místnost" "Byli jste odhlášeni z relace" "Vzhled" @@ -208,9 +202,7 @@ "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" @@ -220,6 +212,7 @@ "Místnost opuštěna" "Opustit prostor" "Pozvánka odmítnuta" + "Tmavé" "Chyba dešifrování" "Popis" "Možnosti pro vývojáře" @@ -245,7 +238,6 @@ Důvod: %1$s." "Selhalo" "Oblíbené" "Oblíbené" - "Synchronizace oznámení…" "Soubor" "Soubor smazán" "Soubor uložen" @@ -259,6 +251,7 @@ 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í" @@ -327,6 +320,7 @@ 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" @@ -383,6 +377,7 @@ Důvod: %1$s." "Doporučeno" "Návrhy" "Synchronizace" + "Systém" "Text" "Oznámení třetích stran" "Vlákno" @@ -471,9 +466,6 @@ 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" @@ -481,15 +473,8 @@ Opravdu chcete pokračovat?" "Možnosti" "Odstranit %1$s" "Nastavení" - "Nikdo nesdílí svou polohu" - "Sdílení aktuální polohy" - - "%1$d osoba" - "%1$d osoby" - "%1$d lidí" - - "Na mapě" "Výběr média se nezdařil, zkuste to prosím znovu." + "Vítejte zpět" "Přidržte zprávu a vyberte „%1$s“, kterou chcete zahrnout sem." "Připněte důležité zprávy, aby je bylo možné snadno najít" @@ -514,7 +499,6 @@ Opravdu chcete pokračovat?" "Zpráva v %1$s" "Rozbalit" "Zmenšit" - "Sdílení aktuální polohy" "Již si prohlížíte tuto místnost!" "%1$s z %2$s" "%1$s Připnuté zprávy" 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 00e2a39297..9c4b04b9d5 100644 --- a/libraries/ui-strings/src/main/res/values-cy/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cy/translations.xml @@ -198,6 +198,7 @@ "Wedi gadael yr ystafell" "Gofod chwith" "Wedi gwrthod y gwahoddiad" + "Tywyll" "Gwall dadgryptio" "Disgrifiad" "Dewisiadau datblygwr" @@ -234,6 +235,7 @@ 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…" @@ -307,6 +309,7 @@ Rheswm: %1$s." "%1$d ateb" "Yn ymateb i %1$s" + "Adrodd ar wall" "Adrodd am broblem" "Adroddiad wedi ei gyflwyno" "Golygydd testun cyfoethog" @@ -357,6 +360,7 @@ 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 ce79b8972b..e12e049469 100644 --- a/libraries/ui-strings/src/main/res/values-da/translations.xml +++ b/libraries/ui-strings/src/main/res/values-da/translations.xml @@ -14,10 +14,9 @@ "Krypteringsoplysninger" "Udvid tekstfeltet for beskeder" "Skjul adgangskode" - "Info" "Deltag i opkald" "Hop til bunden" - "Flyt kortet til min placering" + "Flyt kortet til min lokation" "Kun omtaler" "Lyd slået fra" "Nye omtaler" @@ -49,10 +48,8 @@ "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" @@ -90,15 +87,12 @@ "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" @@ -166,7 +160,7 @@ "Send talebesked" "Del" "Del link" - "Del placering live" + "Del liveplacering" "Vis" "Log ind igen" "Fjern denne enhed" @@ -198,6 +192,7 @@ "Avancerede indstillinger" "et billede" "Analyse-værktøj" + "Synkroniserer notifikationer…" "Du forlod rummet" "Du blev logget ud af sessionen" "Udseende" @@ -205,9 +200,7 @@ "Beta" "Blokerede brugere" "Bobler" - "Opkald afvist" "Opkald startet" - "Du afviste et opkald" "Backup af samtale" "Kopieret til udklipsholder" "Ophavsret" @@ -217,6 +210,7 @@ "Forlod rummet" "Forlod klynge" "Invitationen blev afvist" + "Mørkt tema" "Fejl under dekryptering" "Beskrivelse" "Indstillinger for udviklere" @@ -242,7 +236,6 @@ "Mislykkedes" "Favorit" "Favoritmarkeret" - "Synkroniserer notifikationer…" "Fil" "Fil slettet" "Fil gemt" @@ -256,6 +249,7 @@ "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" @@ -321,6 +315,7 @@ "%1$d svar" "Svarer til %1$s" + "Rapportér en fejl" "Anmeld et problem" "Anmeldelsen er indsendt" "Rich text editor" @@ -362,7 +357,7 @@ "Noget gik galt" "Vi stødte på et problem. Prøv venligst igen." "Klynge" - "Medlemmer af klyngen" + "Medlemmer af rummet" "Hvad handler denne klynge om?" "%1$d Klynge" @@ -374,6 +369,7 @@ "Forslag" "Forslag" "Synkroniserer" + "System" "Tekst" "Tredjepartsmeddelelser" "Tråd" @@ -469,14 +465,8 @@ 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." + "Velkommen tilbage" "Tryk på en besked og vælg \"%1$s\" for at inkludere den her." "Fastgør vigtige beskeder, så de let kan opdages" @@ -500,15 +490,14 @@ 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 placering" - "Del min placering" + "Del lokation" + "Del min lokation" "Åbn i Apple Maps" "Åbn i Google Maps" "Åbn i OpenStreetMap" @@ -524,7 +513,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." - "Placering" + "Lokation" "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 5b7f52cf4e..1d0f26085e 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -1,7 +1,6 @@ "Reaktion hinzufügen: %1$s" - "Adresse" "Avatar" "Nachrichtentextfeld minimieren" "Löschen" @@ -27,12 +26,9 @@ "Pausieren" "Sprachnachricht, Dauer:%1$s, aktuelle Position: %2$s" "PIN-Feld" - "Fixierter Standort" "Abspielen" - "Wiedergabegeschwindigkeit" "Umfrage" "Umfrage beendet" - "QR-Code" "Reagiere mit %1$s" "Mit anderen Emojis reagieren" "Gelesen von %1$s und %2$s" @@ -46,11 +42,9 @@ "Entferne Reaktionen mit %1$s" "Avatar" "Dateien senden" - "Standort des Absenders" "Zeitlich begrenzte Handlung erforderlich, du hast eine Minute Zeit zur Verifizierung" "Passwort anzeigen" "Anruf starten" - "Sprachanruf starten" "Stillgelegter Chat" "Nutzer-Avatar" "Nutzer-Menü" @@ -120,7 +114,6 @@ "Space verlassen" "Mehr laden…" "Konto verwalten" - "Konto & Geräte verwalten" "Geräte verwalten" "Chats und Gruppen konfigurieren" "Nachricht" @@ -160,18 +153,16 @@ "Sprachnachricht senden" "Teilen" "Link teilen" - "Live-Standort teilen" "Zeige" "Erneut anmelden" - "Dieses Gerät entfernen" - "Dieses Gerät trotzdem entfernen" + "Abmelden" + "Trotzdem abmelden" "Überspringen" "Start" "Chat starten" "Neu beginnen" "Verifizierung starten" "Tippe, um die Karte zu laden" - "Beenden" "Foto aufnehmen" "Für Optionen tippen" "Übersetzen" @@ -209,6 +200,7 @@ "Hat den Chat verlassen" "Space verlassen" "Einladung abgelehnt" + "Dunkel" "Dekodierungsfehler" "Beschreibung" "Entwickleroptionen" @@ -224,7 +216,6 @@ "Leere Datei" "Verschlüsselung" "Verschlüsselung aktiviert" - "Endet um %1$s" "PIN eingeben" "Fehler" "Es ist ein Fehler aufgetreten. Du erhältst eventuell keine Benachrichtigungen für neue Nachrichten. Bitte behebe den Fehler in den Einstellungen. @@ -234,7 +225,6 @@ Grund: %1$s." "Fehlgeschlagen" "Favorit" "Favorisiert" - "Benachrichtigungen werden synchronisiert…" "Datei" "Datei wurde gelöscht" "Datei gespeichert" @@ -248,11 +238,10 @@ 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" - "Live-Standort" - "Live-Standort teilen beendet" "Laden…" "Mehr wird geladen…" @@ -279,7 +268,6 @@ Grund: %1$s." "Offline" "Open-Source-Lizenzen" "oder" - "Weitere Optionen" "Passwort" "Personen" "Permalink" @@ -297,10 +285,8 @@ Grund: %1$s." "Vorbereitung läuft …" "Datenschutz­erklärung" - "Privat" "Privater Chat" "Privater Space" - "Öffentlich" "Öffentlicher Chat" "Öffentlicher Space" "Reaktion" @@ -314,6 +300,7 @@ Grund: %1$s." "%1$d Antworten" "%1$s antworten" + "Einen Fehler melden" "Ein Problem melden" "Bericht eingereicht" "Rich-Text-Editor" @@ -348,14 +335,12 @@ Grund: %1$s." "Einstellungen" "Space teilen" "Neue Mitglieder sehen den Nachrichtenverlauf" - "Geteilter Live-Standort" "Geteilter Standort" "Gemeinsamer Space" - "Gerät entfernen" + "Abmelden" "Es ist ein Fehler aufgetreten." "Wir haben ein Problem festgestellt. Bitte versuch es erneut." "Space" - "Space Mitglieder" "Worum geht es hier?" "%1$d Space" @@ -367,16 +352,16 @@ Grund: %1$s." "Empfohlen" "Vorschläge" "Synchronisieren" + "System" "Text" "Hinweise von Drittanbietern" "Thread" - "Threads" "Thema" "Worum geht is in diesem Chat?" "Entschlüsselung nicht möglich" "Von einem ungesicherten Gerät gesendet" "Du hast keinen Zugriff auf diese Nachricht." - "Die verifizierte Identität des Senders wurde zurückgesetzt" + "Die verifizierte Identität des Senders hat sich geändert" "Einladungen konnten nicht an einen oder mehrere Nutzer gesendet werden." "Einladung(en) konnte(n) nicht gesendet werden" "Entsperren" @@ -401,19 +386,17 @@ Grund: %1$s." "Sprachnachricht" "Warten…" "Warte auf diese Nachricht" - "Warten auf Live-Standort…" "Jeder kann den Nachrichtenverlauf sehen" "Du" "%1$s (%2$s) hat diese Nachricht geteilt, weil du nicht im Chat warst, als sie verschickt wurde." "Diese Nachricht wurde von %1$s weitergeleitet, da du zum Zeitpunkt des Versands kein Mitglied der Gruppe warst." "Diese Gruppe wurde so konfiguriert, dass neue Mitglieder den vergangenen Nachrichtenverlauf lesen können. %1$s" - "Die Identität von %1$s wurde zurückgesetzt. %2$s" - "Die Identität von %1$s %2$s wurde zurückgesetzt. %3$s" + "%1$s\'s Identität has sich geändert. %2$s" + "%1$s\'s %2$s Identität hat sich geändert. %3$s" "(%1$s)" - "Die Identität von %1$s wurde zurückgesetzt." - "Die Identität von %1$s %2$s wurde zurückgesetzt. %3$s" + "Die Identität von %1$s hat sich geändert." + "Die Identität von %1$s\'s %2$s hat sich geändert. %3$s" "Verifizierung zurückziehen" - "Zugriff erlauben" "Der Link %1$s führt dich zu einer anderen Seite %2$s. Möchtest du wirklich fortfahren?" @@ -443,7 +426,6 @@ 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." @@ -470,8 +452,8 @@ Möchtest du wirklich fortfahren?" "%1$d fixierte Nachrichten" "Fixierte Nachrichten" - "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." + "Du wirst jetzt zu deinem %1$s Konto geleitet, um deine Identität zurückzusetzen. Danach wirst du zur App zurückgebracht." + "Kannst du das nicht bestätigen? Gehe zu deinem Konto, um deine 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." "Deine Nachricht wurde nicht gesendet, da die verifizierte Identität von %1$s zurückgesetzt wurde" @@ -498,15 +480,12 @@ Möchtest du wirklich fortfahren?" "In Google Maps öffnen" "In OpenStreetMap öffnen" "Diesen Standort teilen" - "Optionen zum Teilen" "Von dir erstellte oder beigetretene Spaces." "%1$s • %2$s" "Erstelle einen Space, um Chats zu organisieren" "%1$s Space" "Spaces" - "Geteilt %1$s" - "Auf der Karte" - "Die Nachricht wurde nicht gesendet, da die verifizierte digitale Identität von %1$s zurückgesetzt wurde." + "Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat." "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" @@ -516,5 +495,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 digitale Identität verifizieren muss." + "Diese Nachricht wurde entweder blockiert, weil du dein Gerät nicht verifiziert hast oder weil der Absender deine 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 ec14b73e04..ee4d782595 100644 --- a/libraries/ui-strings/src/main/res/values-el/translations.xml +++ b/libraries/ui-strings/src/main/res/values-el/translations.xml @@ -120,7 +120,6 @@ "Αποχώρηση από τον χώρο" "Φόρτωσε περισσότερα" "Διαχείριση λογαριασμού" - "Διαχείριση λογαριασμού και συσκευών" "Διαχείριση συσκευών" "Διαχείριση αίθουσών" "Στείλε" @@ -171,7 +170,6 @@ "Ξανά από την αρχή" "Έναρξη επαλήθευσης" "Πάτα για φόρτωση χάρτη" - "Διακοπή" "Τράβηξε φωτογραφία" "Πάτα για επιλογές" "Μετάφραση" @@ -192,6 +190,7 @@ "Ρυθμίσεις για προχωρημένους" "μια εικόνα" "Στατιστικά στοιχεία" + "Συγχρονισμός ειδοποιήσεων…" "Αποχωρήσατε από την αίθουσα" "Αποσυνδεθήκατε από την περίοδο λειτουργίας" "Εμφάνιση" @@ -209,6 +208,7 @@ "Αποχώρησε από την αίθουσα" "Αποχωρήσατε από τον χώρο" "Η πρόσκληση απορρίφθηκε" + "Σκοτεινό" "Σφάλμα αποκρυπτογράφησης" "Περιγραφή" "Επιλογές προγραμματιστή" @@ -224,7 +224,6 @@ "Κενό αρχείο" "Κρυπτογράφηση" "Η κρυπτογράφηση ενεργοποιήθηκε" - "Τελειώνει στις %1$s" "Εισήγαγε το PIN σου" "Σφάλμα" "Παρουσιάστηκε σφάλμα, ενδέχεται να μην λαμβάνεις ειδοποιήσεις για νέα μηνύματα. Αντιμετώπισε το πρόβλημα με τις ειδοποιήσεις από τις ρυθμίσεις. @@ -234,7 +233,6 @@ "Απέτυχε" "Αγαπημένο" "Είναι αγαπημένο" - "Συγχρονισμός ειδοποιήσεων…" "Αρχείο" "Το αρχείο διαγράφηκε" "Το αρχείο αποθηκεύτηκε" @@ -248,11 +246,10 @@ "Αυτό το Matrix ID δεν μπορεί να βρεθεί, επομένως η πρόσκληση ενδέχεται να μην ληφθεί." "Αποχώρηση από την αίθουσα" "Αποχωρείτε από τον χώρο" + "Φωτεινό" "Η γραμμή αντιγράφηκε στο πρόχειρο" "Ο σύνδεσμος αντιγράφηκε στο πρόχειρο" "Σύνδεση νέας συσκευής" - "Ζωντανή τοποθεσία" - "Η ζωντανή τοποθεσία έληξε" "Φόρτωση…" "Φόρτωση περισσότερων…" @@ -314,6 +311,7 @@ "%1$d απαντήσεις" "Απάντηση σε %1$s" + "Αναφορά σφάλματος" "Αναφορά προβλήματος" "Η αναφορά υποβλήθηκε" "Επεξεργαστής εμπλουτισμένου κειμένου" @@ -348,7 +346,6 @@ "Ρυθμίσεις" "Κοινή χρήση χώρου" "Τα νέα μέλη βλέπουν το ιστορικό" - "Κοινόχρηστη ζωντανή τοποθεσία" "Κοινόχρηστη τοποθεσία" "Κοινόχρηστος χώρος" "Αφαίρεση συσκευής" @@ -367,10 +364,10 @@ "Προτεινόμενο" "Προτάσεις" "Συγχρονισμός" + "Σύστημα" "Κείμενο" "Ειδοποιήσεις τρίτων" "Νήμα" - "Νήματα" "Θέμα" "Τι αφορά αυτή η αίθουσα;" "Δεν είναι δυνατή η αποκρυπτογράφηση" @@ -401,7 +398,6 @@ "Φωνητικό μήνυμα" "Αναμονή…" "Αναμονή για αυτό το μήνυμα" - "Αναμονή για ζωντανή τοποθεσία…" "Οποιοσδήποτε μπορεί να δει το ιστορικό" "Εσύ" "%1$s (%2$s) μοιράστηκε αυτό το μήνυμα, καθώς δεν ήσασταν στην αίθουσα όταν στάλθηκε." @@ -463,6 +459,7 @@ "Αφαίρεση %1$s" "Ρυθμίσεις" "Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά." + "Καλώς ήρθατε ξανά" "Πάτα σε ένα μήνυμα και επέλεξε «%1$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 b03223a70e..fa0bc700cb 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -165,6 +165,7 @@ "Solicitud cancelada" "Saliste de la sala" "Invitación rechazada" + "Oscuro" "Error de descifrado" "Opciones de desarrollador" "ID de dispositivo" @@ -200,6 +201,7 @@ 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…" @@ -249,6 +251,7 @@ 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" @@ -278,6 +281,7 @@ 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 f1c7d8d225..53f2178a1a 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -9,13 +9,11 @@ "%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" "Nihuta kaart minu asukohta" @@ -29,12 +27,10 @@ "Peata" "Häälsõnum, kestus:%1$s, praegune asukoht: %2$s" "PIN-koodi väli" - "Esiletõstetud asukoht" "Esita" "Taasesituse kiirus" "Küsitlus" "Lõppenud küsitlus" - "Asukoht: %1$s" "QR-kood" "Reageeri emotikoniga %1$s" "Reageeri mõne muu emotikoniga" @@ -49,16 +45,9 @@ "Eemalda reageerimine: %1$s" "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üü" @@ -76,7 +65,6 @@ "Helista" "Loobu" "Hetkel jäta tegemata" - "Vali fail" "Vali foto" "Selge" "Sulge" @@ -96,15 +84,12 @@ "Eemalda konto kasutusest" "Keeldu" "Keeldu ja blokeeri" - "Kustuta" - "Kustuta kasutajakonto" "Kustuta küsitlus" "Eemalda kõik valikud" "Lülita välja" "Loobu" "Lõpeta" "Valmis" - "Laadi alla" "Muuda" "Muuda selgitust" "Muuda küsitlust" @@ -132,7 +117,6 @@ "Lahku kogukonnast" "Näita veel" "Halda kasutajakontot" - "Halda kasutajakontosid ja seadmeid" "Halda seadmeid" "Halda jututuba" "Saada sõnum" @@ -172,18 +156,16 @@ "Saada häälsõnum" "Jaga" "Jaga linki" - "Jaga asukohta reaalajas" "Näita" "Logi uuesti sisse" - "Eemalda see seade" - "Eemalda see seade ikkagi" + "Logi välja" + "Ikkagi logi välja" "Jäta vahele" "Alusta" "Alusta vestlust" "Alusta uuesti" "Alusta verifitseerimist" "Kaardi laadimiseks klõpsa" - "Lõpeta" "Pildista" "Valikuteks klõpsa" "Tõlgi" @@ -211,9 +193,7 @@ "Beetaversioon" "Blokeeritud kasutajad" "Mullid" - "Osapool keeldus kõnest" "Kõne algas" - "Sa keeldusid kõnest" "Vestluse varukoopia" "Kopeeritud lõikelauale" "Autoriõigused" @@ -223,6 +203,7 @@ "Lahkus jututoast" "Lahkus kogukonnast" "Keeldusid kutsest" + "Tume" "Dekrüptimisviga" "Kirjeldus" "Arendaja valikud" @@ -238,7 +219,6 @@ "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. @@ -248,7 +228,6 @@ Põhjus: %1$s." "Ei õnnestunud" "Lemmik" "Märgitud lemmikuks" - "Sünkroonin teavitusi…" "Fail" "Fail on kustutatud" "Fail on salvestatud" @@ -262,11 +241,10 @@ Põhjus: %1$s." "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…" @@ -293,7 +271,6 @@ Põhjus: %1$s." "Võrgust väljas" "Avatud lähtekoodiga litsentsid" "või" - "Muud valikud" "Salasõna" "Inimesed" "Püsilink" @@ -328,6 +305,7 @@ Põhjus: %1$s." "%1$d vastust" "Vastates kasutajale %1$s" + "Teata veast" "Teata veast" "Veateade on saadetud" "Vormindatud teksti toimeti" @@ -362,10 +340,9 @@ Põhjus: %1$s." "Seadistused" "Jaga kogukonda" "Uued liikmed näevad ajalugu" - "Reaalajas jagatud asukoht" "Jagatud asukoht" "Jagatud kogukond" - "Seade on eemaldamisel" + "Logime välja" "Midagi läks valesti" "Tekkis viga. Palun proovi uuesti." "Kogukond" @@ -381,16 +358,16 @@ Põhjus: %1$s." "Soovitatud" "Soovitused" "Sünkroniseerime" + "Süsteem" "Tekst" "Kolmandate osapoolte teatised" "Jutulõng" - "Jutulõngad" "Teema" "Mis on selle jututoa mõte?" "Dekrüptimine ei olnud võimalik" "Saadetud ebaturvalisest seadmest" "Sul pole ligipääsu antud sõnumile" - "Saatja digitaalse identiteet on lähtestatud" + "Saatja verifitseeritud identiteet on lähtestatud" "Kutset polnud võimalik saata ühele või enamale kasutajale." "Kutse(te) saatmine ei õnnestunud" "Eemalda lukustus" @@ -415,19 +392,17 @@ 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 digitaalse identiteet on lähtestatud. %2$s" - "Kasutaja %1$s %2$s digitaalse identiteet on lähtestatud. %3$s" + "Kasutaja %1$s võrguidentiteet on lähtestatud. %2$s" + "Kasutaja %1$s %2$s võrguidentiteet on lähtestatud. %3$s" "(%1$s)" - "%1$s kasutaja digitaalse identiteet on lähtestatud." - "%1$s kasutaja (ksutajanimega %2$s) digitaalse identiteet on lähtestatud. %3$s" + "%1$s kasutaja verifitseeritud identiteet on lähtestatud." + "%1$s kasutaja (%2$s kasutajanimi) verifitseeritud 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?" @@ -457,7 +432,6 @@ 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." @@ -469,9 +443,6 @@ 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" @@ -479,13 +450,6 @@ Kas sa oled kindel, et soovid jätkata?" "Valikud" "Kustuta: %1$s" "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" @@ -494,11 +458,11 @@ Kas sa oled kindel, et soovid jätkata?" "%1$d esiletõstetud sõnumit" "Esiletõstetud sõnumid" - "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." + "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." "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 digitaalse identiteet on lähtestatud." + "Sinu sõnum on saatmata, kuna kasutaja %1$s verifitseeritud 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" @@ -510,7 +474,6 @@ 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" @@ -522,16 +485,13 @@ Kas sa oled kindel, et soovid jätkata?" "Ava Apple Mapsis" "Ava Google Mapsis" "Ava OpenStreetMapis" - "Jaga valitud asukohta" - "Jagamise valikud" + "Jaga seda asukohta" "Sinu loodud kogukonnad ning need, millega oled liitunud." "%1$s • %2$s" "Jututubade haldamiseks võid luua kogukondi" "Kogukond: %1$s" "Kogukonnad" - "Jagatud %1$s" - "Kaardil" - "Sõnum on saatmata, kuna kasutaja %1$s digitaalse identiteet on lähtestatud." + "Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud 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" @@ -541,5 +501,5 @@ 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 sinu digitaalset identiteeti verifitseerinud, siis sõnumi näitamine on blokeeritud." + "Kuna seade on verifitseerimata või saatja pole sind verifitseerinud, siis sõnumi näitamine on blokeeritud." 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 0c046b8bb4..bef5f1a4b5 100644 --- a/libraries/ui-strings/src/main/res/values-eu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-eu/translations.xml @@ -169,6 +169,7 @@ "Eskaera bertan behera utzi da" "Gelatik atera da" "Gonbidapenari uko egin zaio" + "Iluna" "Deszifratze-errorea" "Garapen aukerak" "Gailuaren IDa" @@ -203,6 +204,7 @@ 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…" @@ -254,6 +256,7 @@ 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" @@ -285,6 +288,7 @@ 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 c14f281f0d..fbe716bc49 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,6 +185,7 @@ "اتاق را ترک کرد" "فضا را ترک کرد" "دعوت لغو شد" + "تیره" "خطای رمزگشایی" "توضیح" "گزینه‌های توسعه دهنده" @@ -219,14 +220,11 @@ "شناسهٔ ماتریکس نتوانست پیدا شود. ممکن است دعوت نرسیده باشد." "ترک کردن اتاق" "ترک کردن فضا" + "روشن" "خط در تخته‌گیره رونوشت شد" "پیوند در تخته‌گیره رونوشت شد" "بار کردن…" "بار کردن بیش‌تر…" - - "%d دیگر" - "%d دیگران" - "%1$d عضو" "%1$d عضو" @@ -272,6 +270,7 @@ "کلید بازیابی" "تازه سازی…" "پاسخ دادن به %1$s" + "گزارش یک اشکال" "گزارش مشکل" "گزارش ثبت شد" "ویرایشگر متن غنی" @@ -306,6 +305,7 @@ "موفّقیت" "پیشنهادها" "هم‌گام ساختن" + "سامانه" "متن" "تذکّرهای سوم‌شخص" "رشته" @@ -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 449fd0f352..7be84a8716 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -9,16 +9,13 @@ "%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" @@ -35,7 +32,6 @@ "Toistonopeus" "Kysely" "Päättynyt kysely" - "Sijainti: %1$s" "QR-koodi" "Lisää reaktio: %1$s" "Reagoi muilla emojeilla" @@ -51,15 +47,10 @@ "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" @@ -77,7 +68,6 @@ "Soita" "Peruuta" "Peruuta toistaiseksi" - "Valitse tiedosto" "Valitse kuva" "Tyhjennä" "Sulje" @@ -97,15 +87,12 @@ "Deaktivoi tili" "Hylkää" "Hylkää ja estä" - "Poista" - "Poista tili" "Poista kysely" "Poista kaikki valinnat" "Poista käytöstä" "Hylkää" "Sulje" "Valmis" - "Lataa" "Muokkaa" "Muokkaa kuvatekstiä" "Muokkaa kyselyä" @@ -205,6 +192,7 @@ "Edistyneet asetukset" "kuva" "Analytiikka" + "Synkronoidaan ilmoituksia…" "Poistuit huoneesta" "Sinut kirjattiin ulos istunnosta" "Ulkoasu" @@ -212,9 +200,7 @@ "Beeta" "Estetyt käyttäjät" "Kuplat" - "Puhelu hylätty" "Puhelu alkoi" - "Hylkäsit puhelun" "Keskustelujen varmuuskopiointi" "Kopioitu leikepöydälle" "Tekijänoikeudet" @@ -224,6 +210,7 @@ "Poistuit huoneesta" "Poistuit tilasta" "Kutsu hylätty" + "Tumma" "Salauksen purkuvirhe" "Kuvaus" "Kehittäjän asetukset" @@ -249,7 +236,6 @@ Syy: %1$s." "Epäonnistui" "Lisää suosikkeihin" "Lisätty suosikkeihin" - "Synkronoidaan ilmoituksia…" "Tiedosto" "Tiedosto poistettu" "Tiedosto tallennettu" @@ -263,6 +249,7 @@ 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" @@ -329,6 +316,7 @@ Syy: %1$s." "%1$d vastausta" "Vastataan käyttäjälle %1$s" + "Ilmoita virheestä" "Ilmoita ongelmasta" "Ilmoitus lähetetty" "Rikastettu tekstieditori" @@ -382,6 +370,7 @@ Syy: %1$s." "Ehdotettu" "Ehdotukset" "Synkronoidaan" + "Järjestelmän oletus" "Teksti" "Kolmannen osapuolen ilmoitukset" "Viestiketju" @@ -470,9 +459,6 @@ 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" @@ -480,18 +466,14 @@ Haluatko varmasti jatkaa?" "Vaihtoehdot" "Poista %1$s" "Asetukset" - - "%1$d aste" - "%1$d astetta" - - "Kukaan ei jaa sijaintiaan" - "Jaetaan reaaliaikaista sijaintia" - - "%1$d henkilö" - "%1$d henkilöä" - - "Kartalla" "Median valinta epäonnistui, yritä uudelleen." + "Avaa Element Classic" + "Avaa Element Classic laitteellasi" + "Mene kohtaan \"Asetukset\" > \"Tietoturva ja yksityisyys\"" + "Osiossa \"Salausavainten hallinta\", paina \"Salattujen viestien palautus\"." + "Noudata ohjeita" + "Palaa takaisin %1$s -sovellukseen" + "Tervetuloa takaisin" "Paina viestiä ja valitse “%1$s” lisätäksesi sen tänne." "Kiinnitä tärkeät viestit, jotta ne löytyvät helposti." @@ -515,7 +497,6 @@ Haluatko varmasti jatkaa?" "Viesti huoneessa %1$s" "Laajenna" "Pienennä" - "Jaetaan reaaliaikaista sijaintia" "Katselet jo tätä huonetta!" "%1$s / %2$s" "Kiinnitetty viesti %1$s" 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 212b5eb79a..bec6d7f4ca 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -9,16 +9,13 @@ "%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" @@ -35,7 +32,6 @@ "Vitesse de lecture" "Sondage" "Sondage terminé" - "Position: %1$s" "Code QR" "Réagir avec %1$s" "Réagir avec d’autres émojis" @@ -51,15 +47,10 @@ "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" @@ -77,7 +68,6 @@ "Appel" "Annuler" "Annuler pour l’instant" - "Choisir un fichier" "Choisir une photo" "Effacer" "Fermer" @@ -97,15 +87,12 @@ "Désactiver le compte" "Refuser" "Refuser et bloquer" - "Supprimer" - "Supprimer le compte" "Supprimer le sondage" "Tout désélectionner" "Désactiver" "Annuler" "Ignorer" "Terminé" - "Télécharger" "Modifier" "Modifier la légende" "Modifier le sondage" @@ -205,6 +192,7 @@ "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" @@ -212,9 +200,7 @@ "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" @@ -224,6 +210,7 @@ "Vous avez quitté le salon" "Vous avez quitté l’espace" "Invitation refusée" + "Sombre" "Erreur de déchiffrement" "Description" "Options pour les développeurs" @@ -249,7 +236,6 @@ Raison : %1$s." "Échec" "Favori" "Ajouté aux favoris" - "Synchronisation des notifications…" "Fichier" "Fichier supprimé" "Fichier enregistré" @@ -263,6 +249,7 @@ 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" @@ -329,6 +316,7 @@ 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" @@ -382,6 +370,7 @@ Raison : %1$s." "Recommandé" "Suggestions" "Synchronisation" + "Système" "Texte" "Avis de tiers" "Fil de discussion" @@ -470,9 +459,6 @@ 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" @@ -480,18 +466,15 @@ Raison : %1$s." "Options" "Supprimer %1$s" "Paramètres" - - "%1$d degré" - "%1$d degrés" - - "Personne ne partage sa position" - "Partage de la position en direct" - - "%1$d personne" - "%1$d personnes" - - "Sur la carte" "Échec de la sélection du média, veuillez réessayer." + "Ouvrir Element Classic" + "Ouvrez Element Classic sur votre appareil" + "Aller à Paramètres > Sécurité et vie privée" + "Dans Gestion des clés cryptographiques, sélectionnez Récupération des messages chiffrés" + "Suivez les instructions pour activer votre stockage de clés" + "Revenez à %1$s" + "Activez le stockage de vos clés avant de continuer avec %1$s" + "Bon retour parmi nous" "Cliquez (clic long) sur un message et choisissez « %1$s » pour qu‘il apparaisse ici." "Épinglez les messages importants pour leur donner plus de visibilité" @@ -515,7 +498,6 @@ Raison : %1$s." "Message dans %1$s" "Développer" "Réduire" - "Partage de la position en direct" "Vous êtes déjà dans ce salon!" "%1$s sur %2$s" "%1$s Messages épinglés" 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 369d8ccce4..1bb8ceb426 100644 --- a/libraries/ui-strings/src/main/res/values-hr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hr/translations.xml @@ -1,7 +1,6 @@ "Dodaj reakciju: %1$s" - "Adresa" "Avatar" "Minimiziraj tekstno polje poruke" "Izbriši" @@ -15,7 +14,6 @@ "Pojedinosti o šifriranju" "Proširi tekstno polje poruke" "Sakrij zaporku" - "Informacije" "Pridruži se pozivu" "Idi na dno" "Pomakni kartu na moju lokaciju" @@ -29,12 +27,9 @@ "Pauziraj" "Glasovna poruka, trajanje: %1$s, trenutačno zaustavljeno na: %2$s" "Polje za PIN" - "Prikvačena lokacija" "Reproduciraj" - "Brzina reprodukcije" "Anketa" "Završena anketa" - "QR kod" "Reagiraj s %1$s" "Reagiraj s drugim emotikonima" "Pročitali %1$s i %2$s" @@ -49,13 +44,9 @@ "Ukloni reakciju s %1$s" "Avatar sobe" "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" - "Započni glasovni poziv" "Soba označena za uklanjanje" "Korisnički avatar" "Korisnički izbornik" @@ -67,7 +58,6 @@ "Vaš avatar" "Prihvati" "Dodaj opis" - "Dodaj postojeće sobe" "Dodaj na vremensku traku" "Natrag" "Poziv" @@ -86,28 +76,23 @@ "Kopiraj poveznicu u poruku" "Kopiraj tekst" "Stvori" - "Napravi sobu" - "Stvori prostor" + "Stvori sobu" "Deaktiviraj" "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" "Omogući" "Završi anketu" "Unesite PIN" - "Istražite javne prostore" "Završi" "Zaboravili ste zaporku?" "Proslijedi" @@ -128,7 +113,6 @@ "Napusti prostor" "Učitaj više" "Upravljanje računom" - "Upravljanje računom i uređajima" "Upravljanje uređajima" "Upravljaj sobama" "Poruka" @@ -168,18 +152,16 @@ "Pošalji glasovnu poruku" "Podijeli" "Podijeli poveznicu" - "Dijeljenje lokacije uživo" "Prikaži" "Ponovno se prijavite" - "Ukloni ovaj uređaj" - "Ukloni ovaj uređaj svejedno" + "Odjava" + "Svejedno se odjavi" "Preskoči" "Započni" "Započni razgovor" "Kreni ispočetka" "Započni provjeru" "Dodirnite za učitavanje karte" - "Zaustavi" "Uslikaj" "Dodirnite za mogućnosti" "Prevedi" @@ -207,18 +189,16 @@ "Beta" "Blokirani korisnici" "Mjehurići" - "Poziv je odbijen" "Poziv je započeo" - "Odbili ste poziv" "Sigurnosna kopija razgovora" "Kopirano u međuspremnik" "Autorsko pravo" "Stvaranje sobe…" - "Stvaranje prostora…" "Zahtjev je otkazan" "Napustio/la je sobu" "Napušteni prostor" "Poziv je odbijen" + "Tamno" "Pogreška kod dešifriranja" "Opis" "Mogućnosti za razvojne inženjere" @@ -234,7 +214,6 @@ "Prazna datoteka" "Šifriranje" "Šifriranje je omogućeno" - "Završava u %1$s" "Unesite svoj PIN" "Pogreška" "Došlo je do pogreške; možda nećete primati obavijesti za nove poruke. Riješite problem s obavijestima u postavkama. @@ -244,7 +223,6 @@ Razlog: %1$s ." "Nije uspjelo" "Favorit" "Označeno kao favorit" - "Sinkronizacija obavijesti…" "Datoteka" "Datoteka je izbrisana" "Datoteka je spremljena" @@ -258,11 +236,10 @@ 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" - "Lokacija uživo" - "Prikaz lokacije uživo je završio" "Učitavanje…" "Učitava se još…" @@ -291,7 +268,6 @@ Razlog: %1$s ." "Izvan mreže" "Licencije otvorenog koda" "ili" - "Ostale opcije" "Zaporka" "Osobe" "Stalna poveznica" @@ -310,10 +286,8 @@ Razlog: %1$s ." "Priprema…" "Pravilnik o zaštiti privatnosti" - "Privatno" "Privatna soba" "Privatni prostor" - "Javno" "Javna soba" "Javni prostor" "Reakcija" @@ -321,17 +295,16 @@ Razlog: %1$s ." "Razlog" "Ključ za oporavak" "Osvježavanje…" - "U tijeku je uklanjanje…" "%1$d odgovor" "%1$d odgovora" "%1$d odgovora" "Odgovara korisniku %1$s" + "Prijavi pogrešku" "Prijavi problem" "Prijava je podnesena" "Uređivač obogaćenog teksta" - "Uloga" "Soba" "Naziv sobe" "npr. naziv vašeg projekta" @@ -348,10 +321,6 @@ Razlog: %1$s ." "Sigurnost" "Vidio/la" "Odaberi račun" - - "%1$d odabrano" - "%1$d odabrano" - "Pošalji" "Slanje…" "Slanje nije uspjelo" @@ -362,15 +331,12 @@ Razlog: %1$s ." "URL poslužitelja" "Postavke" "Podijeli prostor" - "Novi članovi vide povijest" - "Dijeljena lokacija uživo" "Podijeljena lokacija" "Zajednički prostor" - "Uklanjanje uređaja" + "Odjava je u tijeku" "Nešto je pošlo po zlu" "Naišli smo na problem. Pokušajte ponovno." "Prostor" - "Članovi prostora" "O čemu se radi u ovom prostoru?" "%1$d prostor" @@ -380,13 +346,12 @@ Razlog: %1$s ." "Započinjanje razgovora…" "Naljepnica" "Uspjeh" - "Preporučeno" "Prijedlozi" "Sinkronizacija" + "Sustav" "Tekst" "Obavijesti trećih strana" "Nit" - "Niti" "Tema" "O čemu je ova soba?" "Nije moguće dešifrirati" @@ -417,11 +382,7 @@ Razlog: %1$s ." "Glasovna poruka" "Čekanje…" "Čekam ovu poruku" - "Čekanje lokacije uživo…" - "Svatko može vidjeti povijest" "Vi" - "%1$s(%2$s ) je podijelio/la ovu poruku jer niste bili u sobi kada je poslana." - "%1$spodijelio/la je ovu poruku jer nisi bio/la u sobi kada je poslana." "Ova je soba konfigurirana tako da novi članovi mogu čitati stare poruke. %1$s" "Identitet korisnika %1$s je poništen. %2$s" "Identitet korisnika %1$s %2$s je poništen. %3$s" @@ -429,7 +390,6 @@ Razlog: %1$s ." "Identitet korisnika %1$s je poništen." "Identitet korisnika %1$s %2$s je poništen. %3$s" "Povuci provjeru" - "Dopusti pristup" "Poveznica %1$s vodi vas na drugo mrežno mjesto %2$s Jeste li sigurni da želite nastaviti?" @@ -459,7 +419,6 @@ Jeste li sigurni da želite nastaviti?" "%1$s nije mogao pristupiti vašoj lokaciji. Pokušajte ponovno poslije." "Prijenos vaše glasovne poruke nije uspio." "Soba više ne postoji ili pozivnica više ne vrijedi." - "Omogućite GPS za pristup značajkama temeljenim na lokaciji." "Poruka nije pronađena" "%1$s nema dopuštenje za pristup vašoj lokaciji. Pristup možete omogućiti u postavkama." "%1$s nema dopuštenje za pristup vašoj lokaciji. Omogućite pristup u nastavku." @@ -478,14 +437,6 @@ 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" @@ -511,7 +462,6 @@ Jeste li sigurni da želite nastaviti?" "Poruka u sobi %1$s" "Proširi" "Smanji" - "Dijeljenje lokacije uživo" "Već gledam ovu sobu!" "%1$s od %2$s" "%1$s Prikvačene poruke" @@ -524,14 +474,10 @@ Jeste li sigurni da želite nastaviti?" "Otvori u Google Maps" "Otvori u OpenStreetMap" "Podijeli ovu lokaciju" - "Opcije dijeljenja" "Prostori koje ste stvorili ili kojima ste se pridružili." "%1$s • %2$s" - "Stvorite prostore za organizaciju soba" "Prostor %1$s" "Prostori" - "Dijeljeno %1$s" - "Na karti" "Poruka nije poslana jer je poništen potvrđeni identitet korisnika %1$s." "Poruka nije poslana jer %1$s nije potvrdio sve uređaje." "Poruka nije poslana jer niste potvrdili jedan svoj uređaj ili više njih." 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 caca4b5014..9586b392e9 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -9,13 +9,11 @@ "%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" "Térkép áthelyezése a jelenlegi helyre" @@ -34,7 +32,6 @@ "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" @@ -50,15 +47,10 @@ "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ü" @@ -76,7 +68,6 @@ "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" @@ -96,15 +87,12 @@ "Fiók deaktiválása" "Elutasítás" "Elutasítás és letiltás" - "Törlés" - "Fiók 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" @@ -204,6 +192,7 @@ "Speciális beállítások" "egy kép" "Elemzések" + "Értesítések szinkronizálása…" "Elhagyta a szobát" "Ki lett jelentkeztetve a munkamenetből" "Megjelenítés" @@ -211,9 +200,7 @@ "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" @@ -223,6 +210,7 @@ "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" @@ -248,7 +236,6 @@ 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" @@ -262,6 +249,7 @@ 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" @@ -327,6 +315,7 @@ 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ő" @@ -380,6 +369,7 @@ Ok: %1$s." "Javaslat" "Javaslatok" "Szinkronizálás" + "Rendszer" "Szöveg" "Harmadik felek nyilatkozatai" "Üzenetszál" @@ -468,9 +458,6 @@ 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" @@ -478,14 +465,15 @@ Biztos, hogy folytatja?" "Lehetőségek" "Eltávolítás: %1$s" "Beállítások" - "Senki sem osztja meg a tartózkodási helyét" - "Élő helymegosztás" - - "%1$d személy" - "%1$d személy" - - "A térképen" "Nem sikerült kiválasztani a médiát, próbálja újra." + "Nyissa meg az Element Classic alkalmazást" + "Nyissa meg az Element Classic alkalmazást az eszközén" + "Lépjen a Beállítások > Biztonság és adatvédelem menüponthoz" + "A Kriptográfiai kulcsok kezelése részben válassza a Titkosított üzenetek helyreállítása lehetőséget" + "Kövesse az utasításokat a kulcstároló engedélyezéséhez" + "Térjen vissza ide: %1$s" + "Engedélyezze a kulcstárolást a folytatás előtt ide: %1$s" + "Üdvözöljük újra!" "Nyomjon hosszan az üzenetre, és válassza a „%1$s” lehetőséget, hogy itt szerepeljen." "Tűzze ki a fontos üzeneteket, hogy könnyen felfedezhetők legyenek" @@ -509,7 +497,6 @@ Biztos, hogy folytatja?" "Üzenet a következőben: %1$s" "Kibontás" "Csökkentés" - "Élő helymegosztás" "Már ezt a szobát nézi!" "%1$s. / %2$s" "%1$s kitűzött üzenet" 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 51d78530a8..5a095a074f 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" - "Hapus device dari akun" + "Keluar dari akun" "Keluar saja" "Lewati" "Mulai" @@ -185,6 +185,7 @@ "Permintaan dibatalkan" "Keluar dari ruangan" "Undangan ditolak" + "Gelap" "Kesalahan dekripsi" "Deskripsi" "Opsi pengembang" @@ -221,6 +222,7 @@ 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…" @@ -273,6 +275,7 @@ Alasan: %1$s." "%1$d balasan" "Membalas %1$s" + "Laporkan kutu" "Laporkan masalah" "Laporan terkirim" "Penyunting teks kaya" @@ -306,6 +309,7 @@ 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 d1ec78d920..0d73a8c121 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -14,7 +14,6 @@ "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" @@ -49,7 +48,6 @@ "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" @@ -71,7 +69,6 @@ "Chiama" "Annulla" "Annulla per ora" - "Scegli il file" "Scegli foto" "Cancella" "Chiudi" @@ -91,15 +88,12 @@ "Disattiva account" "Rifiuta" "Rifiuta e blocca" - "Elimina" - "Elimina account" "Elimina sondaggio" "Deseleziona tutti" "Disabilita" "Annulla" "Chiudi" "Fine" - "Scarica" "Modifica" "Modifica didascalia" "Modifica sondaggio" @@ -199,6 +193,7 @@ "Impostazioni avanzate" "un\'immagine" "Statistiche di utilizzo" + "Sincronizzazione delle notifiche…" "Hai lasciato la stanza" "Sei stato disconnesso dalla sessione" "Aspetto" @@ -206,9 +201,7 @@ "Beta" "Utenti bloccati" "Fumetti" - "Chiamata rifiutata" "Chiamata avviata" - "Hai rifiutato una chiamata" "Backup della chat" "Copiato negli appunti" "Copyright" @@ -218,6 +211,7 @@ "Hai lasciato la stanza" "Hai lasciato lo spazio" "Invito rifiutato" + "Scuro" "Errore di decrittazione" "Descrizione" "Opzioni sviluppatore" @@ -243,7 +237,6 @@ Motivo:. %1$s" "Fallito" "Preferiti" "Preferita" - "Sincronizzazione delle notifiche…" "File" "File eliminato" "File salvato" @@ -257,6 +250,7 @@ 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" @@ -323,6 +317,7 @@ Motivo:. %1$s" "%1$d risposte" "Risposta a %1$s" + "Segnala un problema" "Segnala un problema" "Segnalazione inviata" "Editor di testo avanzato" @@ -376,6 +371,7 @@ Motivo:. %1$s" "Suggeriti" "Suggerimenti" "Sincronizzazione" + "Sistema" "Testo" "Comunicazioni di terze parti" "Discussione" @@ -464,8 +460,6 @@ 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" @@ -473,14 +467,15 @@ 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." + "Apri Element Classic" + "Apri Element Classic sul tuo dispositivo" + "Vai su Impostazioni > Sicurezza & privacy" + "Nella gestione delle chiavi crittografiche, seleziona Recupero dei messaggi cifrati" + "Segui le istruzioni per abilitare l\'archiviazione delle chiavi" + "Torna a %1$s" + "Abilita l\'archivio delle chiavi prima di procedere con %1$s" + "Bentornato" "Premi su un messaggio e scegli “%1$s” per includerlo qui." "Fissa i messaggi importanti così che possano essere trovati facilmente" @@ -504,7 +499,6 @@ 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 c32cbca475..f77a92966b 100644 --- a/libraries/ui-strings/src/main/res/values-ja/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ja/translations.xml @@ -8,16 +8,13 @@ "%1$d 桁入力済" - "長さ: %1$s" "アバターを編集" "完全なアドレスは %1$s になります。" "暗号化の詳細" "入力欄を拡大" "パスワードを非表示" - "情報" "通話に参加" "一番下へ" - "未読に移動" "現在地に移動" "メンションのみ" "ミュート有効" @@ -34,7 +31,6 @@ "再生速度" "投票" "投票終了" - "位置: %1$s" "QRコード" "リアクション: %1$s" "他の絵文字でリアクション" @@ -49,15 +45,11 @@ "ルームのアバター" "ファイルを送信" "送信者の位置情報" - "%2$s に %1$s が送信" "1分以内に検証を完了してください" - "処置が必要な設定" "パスワードを表示" "通話を開始" "ビデオ通話を開始" "音声通話を開始" - "%1$s のスレッド" - "%1$s のスレッド" "埋没したルーム" "ユーザーのアバター" "ユーザーメニュー" @@ -75,7 +67,6 @@ "通話" "キャンセル" "今回のみキャンセル" - "ファイルを選択" "写真を選択" "クリア" "閉じる" @@ -95,15 +86,12 @@ "アカウントを無効化" "拒否" "拒否してブロック" - "削除" - "アカウントを削除" "投票を削除" "全ての選択を解除" "無効化" "破棄" "無視" "完了" - "ダウンロード" "編集" "キャプションを編集" "投票を編集" @@ -203,16 +191,15 @@ "高度な設定" "画像" "分析" + "通知を同期中…" "あなたがルームを退出" "セッションからログアウトされました" - "テーマ" + "外観" "音声" "ベータ版" "ブロックしたユーザー" "ふきだし" - "通話を拒否しました" "通話を開始しました" - "通話を拒否しました" "チャットをバックアップ" "クリップボードにコピーしました" "著作権" @@ -222,6 +209,7 @@ "ルームを退出しました" "スペースから退出しました" "招待は却下されました" + "ダーク" "復号化エラー" "詳細" "開発者向けオプション" @@ -247,7 +235,6 @@ "失敗" "お気に入り" "お気に入り" - "通知を同期中…" "ファイル" "ファイルを削除しました" "ファイルを保存しました" @@ -261,6 +248,7 @@ "このMatrix IDは見つからないため、招待が届かない可能性があります。" "ルームを退出しています" "スペースを退出しています" + "ライト" "行をクリップボードにコピーしました" "リンクをクリップボードにコピーしました" "新しい端末から接続" @@ -278,7 +266,7 @@ "メッセージアクション" "メッセージの送信に失敗" "メッセージのレイアウト" - "メッセージが削除されました" + "メッセージは削除されました" "モダン" "ミュート" "名前" @@ -292,7 +280,7 @@ "または" "他のオプション" "パスワード" - "人々" + "人" "固定リンク" "権限" "ピン留め" @@ -323,6 +311,7 @@ "%1$d 件の返信" "%1$s に返信" + "バグを報告" "問題を報告" "報告は送信されました" "リッチテキストエディター" @@ -373,6 +362,7 @@ "推奨される" "提案" "同期中" + "システム" "テキスト" "第三者に関する通知" "スレッド" @@ -461,9 +451,6 @@ "申し訳ありません。エラーが発生しました。" "🔐️ %1$s に参加してください" "%1$s で話しましょう: %2$s" - "ライブ位置情報共有" - "位置情報を共有しています" - "%1$s ライブ位置情報" "%1$s Android" "開発者にバグを報告するには端末を振ってください。" "スクリーンショット" @@ -471,18 +458,16 @@ "選択肢" "%1$s を削除" "設定" - "画像を左に回転" - - "%1$d°" - - "写真を編集" - "誰も位置情報を共有していません" - "ライブ位置情報を共有しています" - - "%1$d 人" - - "地図上" "ファイルの選択に失敗しました。再試行してください。" + "Element Classic を開く" + "Element Classic をこの端末で開く" + "「設定- セキュリティとプライバシー」に移動します" + "暗号鍵の管理から、暗号化されたメッセージの回復を選択します" + "指示に従って、鍵の保管庫を有効化してください" + "%1$s に戻ってください" + "%1$s に続行する前に、鍵の保管庫を有効化してください" + "アカウントを確認中" + "おかえりなさい" "メッセージを長押しし \"%1$s\" を選択してください" "重要なメッセージをピン留めして容易に見つけられるようにします" 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 2be4e43d61..b7a8819391 100644 --- a/libraries/ui-strings/src/main/res/values-ka/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ka/translations.xml @@ -122,6 +122,7 @@ "საავტორო უფლება" "ოთახის შექმნა…" "დატოვა ოთახი" + "მუქი" "გაშიფვრის შეცდომა" "დეველოპერის პარამეტრები" "პირდაპირი ჩატი" @@ -144,6 +145,7 @@ "დააინსტალირეთ APK" "ეს Matrix ID ვერ მოიძებნა, ამიტომ მოწვევა შეიძლება არ იყოს მიღებული." "ოთახის დატოვება" + "ღია" "ბმული კოპირებულია გაცვლის ბუფერში" "იტვირთება…" @@ -184,6 +186,7 @@ "განახლება…" "პასუხი %1$s-ს" + "ხარვეზის შეტყობინება" "შეტყობინება პრობლემაზე" "რეპორტი გაგზავნილია" "მდიდარი ტექსტის რედაქტორი" @@ -210,6 +213,7 @@ "წარმატება" "შეთავაზებები" "სინქრონიზაცია" + "სისტემა" "ტექსტი" "მესამე პირის შენიშვნები" "თემა" 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 303e8ba929..4174e4d15d 100644 --- a/libraries/ui-strings/src/main/res/values-ko/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ko/translations.xml @@ -190,6 +190,7 @@ "고급 설정" "이미지" "통계" + "알림 동기화 중…" "방을 떠남" "세션에서 로그아웃되었습니다." "외관" @@ -207,6 +208,7 @@ "방 떠남" "스페이스 떠남" "초대 거부됨" + "다크" "복호화 오류" "설명" "개발자 설정" @@ -232,7 +234,6 @@ "실패" "즐겨찾기" "즐겨찾기 됨" - "알림 동기화 중…" "파일" "파일 삭제됨" "파일 저장됨" @@ -246,6 +247,7 @@ "Matrix ID를 찾을 수 없기 때문에 초대가 수신되지 않을 수도 있습니다." "방을 떠나는 중" "스페이스 떠나는 중" + "라이트" "줄이 클립보드에 복사되었습니다." "링크가 클립보드에 복사됨" "새 기기 연결" @@ -308,6 +310,7 @@ "%1$d 답변" "%1$s님에게 답장하는 중" + "버그 보고" "문제 보고" "보고 제출됨" "리치 텍스트 편집기" @@ -358,6 +361,7 @@ "공유된 스페이스" "제안" "동기화 중" + "시스템" "글자" "제3자 고지" "스레드" @@ -456,6 +460,14 @@ "%1$s 제거" "설정" "미디어 선택에 실패했습니다. 다시 시도해 주세요." + "Element Classic 열기" + "기기에서 Element Classic 앱을 열어 주세요" + "설정 > 보안 및 개인정보 보호로 이동하세요" + "암호화 키 관리에서 \'암호화된 메시지 복구\'를 선택하세요" + "안내에 따라 키 저장소를 활성화해 주세요" + "%1$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 da5281212e..1ebbceedd5 100644 --- a/libraries/ui-strings/src/main/res/values-lt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-lt/translations.xml @@ -126,6 +126,7 @@ "Reakcijos" "Atnaujinama…" "Atsakant %1$s" + "Pranešti apie klaidą" "Skundas pateiktas" "Kambario pavadinimas" "pvz., jūsų projekto pavadinimas" @@ -169,6 +170,7 @@ "%1$s Android" "Papurtykite, kad praneštumėte apie klaidą" "Nepavyko pasirinkti laikmenos, pabandykite dar kartą." + "Sveiki sugrįžę" "Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą." "Nepavyko gauti naudotojo išsamios informacijos." "Bendrinti vietą" diff --git a/libraries/ui-strings/src/main/res/values-nb/translations.xml b/libraries/ui-strings/src/main/res/values-nb/translations.xml index aa594f8027..f563a89258 100644 --- a/libraries/ui-strings/src/main/res/values-nb/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nb/translations.xml @@ -207,6 +207,7 @@ "Forlot rommet" "Forlot område" "Invitasjon avslått" + "Mørk" "Dekrypteringsfeil" "Beskrivelse" "Alternativer for utviklere" @@ -244,6 +245,7 @@ "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" @@ -307,6 +309,7 @@ "%1$d svar" "Svar til %1$s" + "Rapporter en feil" "Rapporter et problem" "Rapport sendt inn" "Redigeringsprogram for rik tekst" @@ -358,6 +361,7 @@ "Foreslått" "Forslag" "Synkroniserer" + "System" "Tekst" "Varsler fra tredjeparter" "Tråd" @@ -451,6 +455,7 @@ Er du sikker på at du vil fortsette?" "Fjern %1$s" "Innstillinger" "Kunne ikke velge medium, prøv igjen." + "Velkommen tilbake" "Trykk på en melding og velg “%1$s” for å inkludere her." "Fest viktige meldinger slik at de lett kan ses" 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 3edeeb00a0..6e136e7d77 100644 --- a/libraries/ui-strings/src/main/res/values-nl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nl/translations.xml @@ -172,6 +172,7 @@ "Kamer maken…" "Heeft de kamer verlaten" "Uitnodiging geweigerd" + "Donker" "Decryptie fout" "Ontwikkelaarsopties" "Apparaat-ID" @@ -199,6 +200,7 @@ 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…" @@ -248,6 +250,7 @@ Reden: %1$s." "%1$d antwoorden" "Reageren op %1$s" + "Een fout melden" "Meld een probleem" "Melding ingediend" "Uitgebreide tekstverwerker" @@ -276,6 +279,7 @@ 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 da3a5c2fea..6812885c9c 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -10,16 +10,13 @@ "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" @@ -33,11 +30,8 @@ "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" @@ -52,16 +46,9 @@ "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" @@ -73,13 +60,11 @@ "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" @@ -94,32 +79,26 @@ "Kopiuj tekst" "Utwórz" "Utwórz pokój" - "Utwórz przestrzeń" "Dezaktywuj" "Dezaktywuj konto" "Odrzuć" "Odrzuć i zablokuj" - "Usuń" - "Usuń konto" "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ś" @@ -135,9 +114,7 @@ "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" @@ -162,7 +139,7 @@ "Zgłoś treść" "Zgłoś rozmowę" "Zgłoś pokój" - "Zresetuj" + "Resetuj" "Zresetuj tożsamość" "Spróbuj ponownie" "Ponów próbę odszyfrowania" @@ -175,21 +152,18 @@ "Wyślij wiadomość głosową" "Wyślij" "Wyślij link" - "Udostępnij lokalizację na żywo" "Pokaż" "Zaloguj się ponownie" - "Usuń to urządzenie" - "Usuń urządzenie mimo to" + "Wyloguj" + "Wyloguj 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" @@ -214,18 +188,16 @@ "Beta" "Zablokowani użytkownicy" "Bąbelki" - "Połączenie odrzucone" "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" @@ -241,7 +213,6 @@ "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. @@ -251,7 +222,6 @@ Powód: %1$s." "Niepowodzenie" "Ulubione" "Ulubione" - "Synchronizuję powiadomienia…" "Plik" "Plik usunięty" "Plik zapisany" @@ -265,11 +235,9 @@ Powód: %1$s." "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…" @@ -284,12 +252,10 @@ 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" @@ -298,7 +264,6 @@ Powód: %1$s." "Offline" "Licencje open-source" "lub" - "Inne opcje" "Hasło" "Osoby" "Link bezpośredni" @@ -317,10 +282,8 @@ Powód: %1$s." "Przygotowuję…" "Polityka prywatności" - "Prywatny" "Pokój prywatny" "Prywatna przestrzeń" - "Publiczny" "Pokój publiczny" "Przestrzeń publiczna" "Reakcja" @@ -328,20 +291,19 @@ 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 twojego projektu" + "np. nazwa projektu" "%1$d Pokój" "%1$d Pokoje" @@ -355,11 +317,6 @@ 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" @@ -370,16 +327,12 @@ 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ń" - "Usuwanie urządzenia" + "Wylogowywanie" "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" @@ -388,13 +341,12 @@ 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ć" @@ -415,7 +367,7 @@ Powód: %1$s." "Zweryfikuj urządzenie" "Zweryfikuj tożsamość" "Zweryfikuj użytkownika" - "Wideo" + "Film" "Wysoka jakość" "Najlepsza jakość, większy rozmiar pliku" "Niska jakość" @@ -425,19 +377,13 @@ Powód: %1$s." "Wiadomość głosowa" "Oczekiwanie…" "Oczekiwanie na tę wiadomość" - "Czekam na lokalizację na żywo…" - "Każdy może zobaczyć historię" "Ty" - "%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 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ć?" @@ -467,7 +413,6 @@ 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." @@ -479,9 +424,6 @@ 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" @@ -489,21 +431,6 @@ Czy na pewno chcesz kontynuować?" "Opcje" "Usuń %1$s" "Ustawienia" - "Obróć obraz w lewo" - - "%1$d stopień" - "%1$d stopnie" - "%1$d stopni" - - "Edytuj zdjęcie" - "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źć" @@ -529,7 +456,6 @@ 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" @@ -541,15 +467,11 @@ Czy na pewno chcesz kontynuować?" "Otwórz w Apple Maps" "Otwórz w Google Maps" "Otwórz w OpenStreetMap" - "Udostępnij wybraną lokalizację" - "Opcje udostępniania" + "Udostępnij tę lokalizację" "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" "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ń." @@ -560,5 +482,5 @@ 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" - "Ta wiadomość została zablokowana, ponieważ urządzenie nie zostało zweryfikowane lub nadawca musi zweryfikować Twoją tożsamość." + "Wiadomość została zablokowana, ponieważ urządzenie nie zostało zweryfikowane lub nadawca musi zweryfikować Twoją tożsamość." 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 f35aca5a58..49351c6b64 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,6 +200,7 @@ "Saiu da sala" "Saiu do espaço" "Convite recusado" + "Escuro" "Erro de descriptografia" "Descrição" "Opções de desenvolvedor" @@ -237,6 +238,7 @@ 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" @@ -298,6 +300,7 @@ Motivo:​ %1$s." "%1$d respostas" "Respondendo a %1$s" + "Denunciar um bug" "Relatar um problema" "Relatório enviado" "Editor de rich text" @@ -348,6 +351,7 @@ 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 0acc939565..56346fe1ac 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" - "Remover este dispositivo" - "Remover mesmo assim" + "Terminar sessão" + "Terminar mesmo assim" "Saltar" "Iniciar" "Iniciar conversa" @@ -195,6 +195,7 @@ "Saíste da sala" "Saíste do espaço" "Convite rejeitado" + "Escuro" "Erro de decifragem" "Descrição" "Opções de programador" @@ -232,6 +233,7 @@ 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…" @@ -289,6 +291,7 @@ Razão: %1$s." "%1$d respostas" "Em resposta a %1$s" + "Comunicar falha" "Comunicar um problema" "Denúncia submetida" "Editor de texto rico" @@ -319,7 +322,7 @@ Razão: %1$s." "Partilhar espaço" "Localização partilhada" "Espaço partilhado" - "A remover dispositivo" + "A terminar sessão" "Algo correu mal" "Encontramos um erro. Por favor, tenta novamente." "Espaço" @@ -332,6 +335,7 @@ Razão: %1$s." "Sucesso" "Sugestões" "A sincronizar…" + "Sistema" "Texto" "Avisos de terceiros" "Tópico" @@ -454,7 +458,7 @@ Tens a certeza de que queres continuar?" "Abrir no Apple Maps" "Abrir no Google Maps" "Abrir no OpenStreetMap" - "Partilhar local selecionado" + "Partilhar este local" "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 dfd68686ce..652a285961 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -1,7 +1,6 @@ "Adăugați o reacție: %1$s" - "Adresă" "Imagine de profil" "Micșorați câmpul mesajului" "Ștergere" @@ -15,7 +14,6 @@ "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" @@ -29,12 +27,9 @@ "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" @@ -49,13 +44,9 @@ "Î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" @@ -67,17 +58,15 @@ "Avatarul dumneavoastră" "Acceptați" "Adăugați o descriere" - "Adăugați camere existente" "Adăugați listei de mesaje" "Înapoi" - "Apelați" + "Apel" "Anulați" "Anulați pentru moment" - "Alegeți fișierul" "Alegeți o fotografie" "Ștergeți" "Închideți" - "Finalizați verificarea" + "Verificare completă" "Confirmați" "Confirmați parola" "Continuați" @@ -88,27 +77,22 @@ "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" - "Terminat" - "Descărcare" + "Efectuat" "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" @@ -128,11 +112,10 @@ "Părăsiți camera" "Părăsiți spațiul" "Încărcați mai mult" - "Gestionați contul" - "Gestionați contul și dispozitivele" - "Gestionați dispozitivele" + "Administrare cont" + "Gestionare dispozitive" "Gestionați camerele" - "Contactați" + "Mesaj" "Minimizați" "Următorul" "Nu" @@ -141,11 +124,11 @@ "Deschideți meniul contextual" "Setări" "Deschideți cu" - "Fixați" + "Fixează" "Raspuns rapid" - "Citați" + "Citat" "Reacționați" - "Respingeți" + "Respinge" "Indepărtați" "Ștergeți descrierea" "Ștergeți mesajul" @@ -156,7 +139,7 @@ "Raportați conținutul" "Raportați conversația" "Raportați camera" - "Resetați" + "Resetare" "Resetați identitatea" "Reîncercați" "Reîncercați decriptarea" @@ -169,8 +152,7 @@ "Trimiteți un mesaj vocal" "Partajați" "Partajați linkul" - "Partajați locația în timp real" - "Afișați" + "Afișare" "Autentificați-vă din nou" "Deconectați-vă" "Deconectați-vă oricum" @@ -180,12 +162,10 @@ "Î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" - "Defixați" + "Defixeaza" "Vizualizați" "Vedeți în lista de mesaje" "Vedeți sursă" @@ -195,8 +175,8 @@ "Upgrade disponibil" "Despre" "Politică de utilizare rezonabilă" - "Adăugare cont" - "Adăugare cont" + "Adăugați un cont" + "Adăugați un alt cont" "Adăugare descriere" "Setări avansate" "o imagine" @@ -208,18 +188,16 @@ "Beta" "Utilizatori blocați" "Baloane" - "Apel respins" - "Apel inițiat" - "Ați respins un apel" + "A început un apel" "Backup conversații" "Copiat în clipboard" "Drepturi de autor" "Se creează camera…" - "Se crează spațiul" "Cerere anulată" - "Cameră părăsită" - "Spațiu părăsit" + "Ați parăsit camera" + "S-a părăsit spațiul" "Invitația a fost refuzată" + "Întunecat" "Eroare de decriptare" "Descriere" "Opțiuni programator" @@ -235,7 +213,6 @@ "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. @@ -245,7 +222,6 @@ Motiv:%1$s." "Eșuat" "Favorite" "Favorită" - "Se sincronizează notificările…" "Fişier" "Fișier șters" "Fișier salvat" @@ -259,11 +235,10 @@ 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ă…" @@ -282,7 +257,7 @@ Motiv:%1$s." "Aspectul mesajelor" "Mesaj șters" "Modern" - "Dezactivare sunet" + "Dezactivați sunetul" "Nume" "%1$s (%2$s)" "Niciun rezultat" @@ -292,7 +267,6 @@ Motiv:%1$s." "Deconectat" "Licențe open source" "sau" - "Alte opțiuni" "Parola" "Persoane" "Permalink" @@ -311,10 +285,8 @@ 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" @@ -322,20 +294,19 @@ 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ă" + "de exemplu, numele proiectului dvs." "%1$d Camera" "%1$d Camere" @@ -349,11 +320,6 @@ 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" @@ -363,16 +329,13 @@ Motiv:%1$s." "Serverul nu poate fi accesat" "Adresa URL a serverului" "Setări" - "Partajare spațiu" - "Membrii noi pot vedea istoricul" - "Locație în timp real partajată" + "Partajați spațiul" "Locație partajată" "Spațiu comun" - "Eliminare în curs" + "Deconectare î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" @@ -382,24 +345,23 @@ 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" "Trimis de pe un dispozitiv nesigur" "Nu aveți acces la acest mesaj" - "Identitatea digitala verificată a expeditorului a fost resetată" + "Identitatea verificată a expeditorului a fost resetată" "Nu am putut trimite invitații unuia sau mai multor utilizatori." "Nu s-a putut trimite invitația (invitațiile)" "Deblocare" - "Activare sunet" - "Apel incompatibil" + "Activați sunetul" + "Apel nesuportat" "Eveniment neacceptat" "Utilizator" "Verificare anulată" @@ -419,19 +381,14 @@ 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" + "Identitatea lui %1$s a fost resetată. %2$s" + "Identitatea %2$s a lui %1$s a fost resetată. %3$s" "(%1$s)" "Identitatea lui %1$s a fost resetată." - "Identitatea digitală %2$s a lui %1$s a fost resetată. %3$s" + "Identitatea %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?" @@ -461,7 +418,6 @@ 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." @@ -473,9 +429,6 @@ 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" @@ -483,14 +436,6 @@ 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ță" @@ -500,11 +445,11 @@ Sunteți sigur că doriți să continuați?" "%1$d Mesaje fixate" "Mesaje fixate" - "Urmează să accesați contul dvs. %1$s pentru a vă reseta identitatea digitală. După aceea, veți fi redirecționat către aplicație." - "Nu puteți confirma? Accesați contul dumneavoastră pentru a vă reseta identitatea digitală." + "Urmează să accesați contul dvs. %1$s pentru a vă reseta identitatea. După aceea, veți fi redirecționat către aplicație." + "Nu puteți confirma? Accesați contul dvs. pentru a vă reseta identitatea." "Retrageți verificarea și trimiteți" "Puteți să vă retrageți verificarea și să trimiteți acest mesaj oricum, sau puteți anula pentru moment și să încercați din nou mai târziu după reverificarea lui %1$s." - "Mesajul dumneavoastră nu a fost trimis deoarece identitatea digitala verificată a lui %1$s s-a schimbat" + "Mesajul dumneavoastră nu a fost trimis deoarece identitatea verificată a lui %1$s s-a schimbat" "Trimiteți mesajul oricum" "%1$s utilizează unul sau mai multe dispozitive neverificate. Puteți trimite mesajul oricum sau puteți anula pentru moment și puteți încerca din nou mai târziu, după ce %2$s își va verifica toate dispozitivele." "Mesajul dvs. nu a fost trimis deoarece %1$s nu si-a verificat toate dispozitivele" @@ -516,7 +461,6 @@ 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" @@ -528,16 +472,12 @@ Sunteți sigur că doriți să continuați?" "Deschideți în Apple Maps" "Deschideți în Google Maps" "Deschideți în OpenStreetMap" - "Partajați locația selectată" - "Opțiuni de partajare" + "Distribuiți această locație" "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 identitatea 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." "Locație" @@ -547,5 +487,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 digitală." + "Acest mesaj a fost blocat fie pentru că nu ați verificat dispozitivul, fie pentru că expeditorul trebuie să vă verifice identitatea." 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 3596bc04fe..5e4d7f9b67 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -15,7 +15,6 @@ "Сведения о шифровании" "Развернуть поле ввода" "Скрыть пароль" - "Информация" "Присоединиться к звонку" "Перейти вниз" "Переместить карту к моему местоположению" @@ -51,7 +50,6 @@ "Отправить файлы" "Местоположение отправителя" "Требуется действие, на которое есть ограничение по времени, у вас есть одна минута для проверки" - "Настройки, требуется действие" "Показать пароль" "Начать звонок" "Начать видеозвонок" @@ -73,7 +71,6 @@ "Позвонить" "Отмена" "Пока отменить" - "Выберите файл" "Выбрать фото" "Очистить" "Закрыть" @@ -93,15 +90,12 @@ "Отключить учётную запись" "Отклонить" "Отклонить и заблокировать" - "Удалить" - "Удалить аккаунт" "Удалить опрос" "Отменить выбор" "Отключить" "Отменить" "Закрыть" "Готово" - "Скачать" "Редактировать" "Изменить подпись" "Редактировать опрос" @@ -201,6 +195,7 @@ "Расширенные настройки" "изображение" "Аналитика" + "Синхронизация уведомлений…" "Вы покинули комнату" "Вы вышли из сессии" "Внешний вид" @@ -208,9 +203,7 @@ "Бета-версия" "Заблокированные пользователи" "Пузыри" - "Вызов отклонен" "Звонок начат" - "Ты отклонил звонок" "Резервная копия чатов" "Скопировано в буфер обмена" "Авторское право" @@ -220,6 +213,7 @@ "Покинул комнату" "Покинуть пространство" "Приглашение отклонено" + "Темная" "Ошибка расшифровки" "Описание" "Для разработчиков" @@ -245,7 +239,6 @@ "Ошибка" "Избранное" "Избранное" - "Синхронизация уведомлений…" "Файл" "Файл удалён" "Файл сохранен" @@ -259,6 +252,7 @@ "Идентификатор Matrix ID не найден, приглашение может быть не получено." "Покидаем комнату" "Покидаем пространство" + "Светлое" "Строка скопирована в буфер обмена" "Ссылка скопирована в буфер обмена" "Привязать новое устройство" @@ -329,6 +323,7 @@ "%1$d ответов" "Отвечает %1$s" + "Сообщить об ошибке" "Сообщить о проблеме" "Отчет отправлен" "Форматирование" @@ -385,6 +380,7 @@ "Рекомендуемые" "Предложения" "Синхронизация" + "Системное" "Текст" "Уведомления о третьих лицах" "Обсуждение" @@ -473,9 +469,6 @@ "Произошла ошибка" "🔐️ Присоединяйтесь ко мне в %1$s" "Привет, давай поболтаем в %1$s: %2$s" - "Отправка местонахождения в реальном времени" - "Определение местоположения в процессе" - "Текущее местоположение %1$s" "%1$s Android" "Встряхните устройство, чтобы сообщить об ошибке" "Скриншот" @@ -483,15 +476,15 @@ "Параметры" "Удалить %1$s" "Настройки" - "Никто не делится своим местоположением" - "Местоположение отправляется в реальном времени" - - "%1$d человек" - "%1$d человек" - "%1$d людей" - - "На карте" "Не удалось выбрать медиа, попробуйте еще раз." + "Открыть Element Classic" + "Откройте Element Classic на своем устройстве." + "Перейдите в Настройки > Безопасность и конфиденциальность" + "В разделе «Управление криптографическими ключами» выбери «Восстановление зашифрованных сообщений»" + "Следуйте инструкциям, чтобы активировать хранилище ключей" + "Вернитесь к %1$s" + "Перед продолжением активируйте хранилище ключей %1$s" + "С возвращением" "Нажмите на сообщение и выберите «%1$s», чтобы добавить его сюда." "Закрепите важные сообщения, чтобы их можно было легко найти" 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 11f4b22005..e154093ddb 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -1,7 +1,6 @@ "Pridať reakciu: %1$s" - "Adresa" "Obrázok" "Minimalizovať textové pole správy" "Vymazať" @@ -28,7 +27,6 @@ "Pozastaviť" "Hlasová správa, dĺžka:%1$s, aktuálna pozícia: %2$s" "Pole PIN" - "Pripnuté miesto" "Prehrať" "Anketa" "Ukončená anketa" @@ -49,8 +47,6 @@ "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" @@ -161,8 +157,8 @@ "Zdieľať odkaz" "Zobraziť" "Prihláste sa znova" - "Odstrániť toto zariadenie" - "Napriek tomu odstrániť toto zariadenie" + "Odhlásiť sa" + "Napriek tomu sa odhlásiť" "Preskočiť" "Spustiť" "Začať konverzáciu" @@ -206,6 +202,7 @@ "Opustil/a miestnosť" "Opustil priestor" "Pozvánka bola odmietnutá" + "Tmavý" "Chyba dešifrovania" "Popis" "Možnosti pre vývojárov" @@ -243,6 +240,7 @@ 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" @@ -308,6 +306,7 @@ 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" @@ -361,6 +360,7 @@ 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 vybranú polohu" + "Zdieľajte túto 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 28dd63d425..dfcd514209 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -187,6 +187,7 @@ "Begäran avbruten" "Lämnade rummet" "Inbjudan avvisad" + "Mörkt" "Avkrypteringsfel" "Beskrivning" "Utvecklaralternativ" @@ -223,6 +224,7 @@ 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 …" @@ -279,6 +281,7 @@ Anledning:%1$s." "%1$d svar" "Svarar till %1$s" + "Rapportera en bugg" "Rapportera ett problem" "Rapport inskickad" "Riktextredigerare" @@ -320,6 +323,7 @@ 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 d500b57507..d92daa6527 100644 --- a/libraries/ui-strings/src/main/res/values-tr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-tr/translations.xml @@ -175,6 +175,7 @@ "İstek iptal edildi" "Sol oda" "Davet reddedildi" + "Koyu" "Şifre çözme hatası" "Geliştirici seçenekleri" "Cihaz Kimliği" @@ -210,6 +211,7 @@ 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…" @@ -262,6 +264,7 @@ Neden: %1$s." "Kurtarma anahtarı" "Yenileniyor…" "Cevaplamak için %1$s" + "Hata bildir" "Sorun bildir" "Rapor gönderildi" "Zengin metin editörü" @@ -293,6 +296,7 @@ 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 d7b18bf1ae..835ded33f9 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -1,7 +1,6 @@ "Додати реакцію: %1$s" - "Адреса" "Аватар" "Згорнути поле тексту повідомлення" "Видалити" @@ -15,7 +14,6 @@ "Подробиці шифрування" "Розгорнути текстове поле повідомлення" "Cховати пароль" - "Інформація" "Приєднатися до виклику" "Перейти вниз" "Перемістити карту до мого місця перебування" @@ -29,12 +27,9 @@ "Пауза" "Голосове повідомлення, тривалість: %1$s, поточна позиція: %2$s" "Поле PIN-коду" - "Закріплена локація" "Відтворити" - "Швидкість програвання" "Опитування" "Опитування завершено" - "QR-код" "Реагувати з%1$s" "Відреагувати іншими смайликами" "Прочитано %1$s та %2$s" @@ -49,13 +44,9 @@ "Прибрати реакцію %1$s" "Аватар кімнати" "Надіслати файли" - "Локація відправника" "Необхідно виконати дію, обмежену в часі, у вас є одна хвилина для верифікації" - "Налаштування, потрібні дії" "Показати пароль" "Розпочати виклик" - "Розпочати відеодзвінок" - "Розпочати голосовий дзвінок" "Кімната більше не використовується" "Аватар користувача" "Меню користувача" @@ -73,7 +64,6 @@ "Зателефонувати" "Скасувати" "Скасувати наразі" - "Вибрати файл" "Вибрати фото" "Очистити" "Закрити" @@ -93,22 +83,18 @@ "Деактивувати обліковий запис" "Відхилити" "Відхилити та заблокувати" - "Видалити" - "Видалити обліковий запис" "Видалити опитування" "Скасувати вибір усіх" "Вимкнути" "Відкинути" "Відхилити" "Готово" - "Завантажити" "Редагувати" "Редагувати підпис" "Редагувати опитування" "Увімкнути" "Завершити опитування" "Введіть PIN-код" - "Дізнатися про публічні простори" "Завершити" "Забули пароль?" "Переслати" @@ -129,7 +115,6 @@ "Вийти з простору" "Завантажити ще" "Керування обліковим записом" - "Керування обліковим записом і пристроями" "Керування пристроями" "Керувати кімнатами" "Написати" @@ -169,7 +154,6 @@ "Надіслати голосове повідомлення" "Поділитися" "Поділитися посиланням" - "Ділитися місцезнаходженням у реальному часі" "Показати" "Увійдіть знову" "Вийти" @@ -180,7 +164,6 @@ "Почати спочатку" "Почати верифікацію" "Натисніть, щоб завантажити мапу" - "Зупинити" "Зробити фото" "Торкніться, щоб переглянути параметри" "Перекласти" @@ -208,9 +191,7 @@ "Бета-версія" "Заблоковані користувачі" "Бульбашки" - "Дзвінок відхилено" "Виклик розпочато" - "Ви відхилили дзвінок" "Резервне копіювання бесіди" "Скопійовано до буферу обміну" "Авторське право" @@ -218,8 +199,8 @@ "Створення простору…" "Запит скасовано" "Виходить з кімнати" - "Вийшов із простору" "Запрошення відхилено" + "Темна" "Помилка розшифрування" "Опис" "Налаштування розробника" @@ -235,7 +216,6 @@ "Порожній файл" "Шифрування" "Шифрування ввімкнено" - "Завершується о %1$s" "Введіть свій PIN-код" "Помилка" "Сталася помилка, ви можете не отримувати сповіщення про нові повідомлення. Усуньте неполадки зі сповіщеннями в налаштуваннях. @@ -245,7 +225,6 @@ "Помилка" "Обране" "Обране" - "Синхронізація сповіщень…" "Файл" "Файл видалено" "Файл збережено" @@ -259,11 +238,10 @@ "Цей Matrix-ID не знайдено, тому запрошення може не бути отримано." "Вихід з кімнати" "Вихід з простору" + "Світла" "Рядок скопійовано до буфера обміну" "Посилання скопійовано в буфер обміну" "Під\'єднати новий пристрій" - "Місцезнаходження в реальному часі" - "Показ місцеперебування наживо завершено" "Завантаження" "Завантаження наступних…" @@ -292,7 +270,6 @@ "Не в мережі" "Ліцензії відкритого коду" "або" - "Інші варіанти" "Пароль" "Люди" "Постійне посилання" @@ -311,10 +288,8 @@ "Приготування…" "Політика конфіденційності" - "Приватний" "Приватна кімната (тільки за запрошенням)" "Приватний простір" - "Публічний" "Загальнодоступна кімната" "Загальнодоступний простір" "Реакція" @@ -329,6 +304,7 @@ "%1$d відповідей" "Відповідь %1$s" + "Повідомити про ваду" "Повідомити про проблему" "Звіт подано" "Багатоформатний текстовий редактор" @@ -349,11 +325,6 @@ "Безпека" "Переглянули" "Вибрати обліковий запис" - - "%1$d вибраний" - "%1$d вибрані" - "%1$d вибрано" - "Надіслати до" "Надсилання…" "Не вдалося надіслати" @@ -363,16 +334,12 @@ "Сервер недоступний" "URL-адреса сервера" "Налаштування" - "Поділитися простором" "Нові учасники бачать історію" - "Поділитися місцезнаходженням в реальному часі" "Поширене розташування" - "Простір спільного користування" "Вихід" "Щось пішло не так" "Ми зіткнулися з проблемою. Будь ласка, повторіть спробу." "Простір" - "Учасники простору" "Про що цей простір?" "%1$d простір" @@ -382,13 +349,12 @@ "Початок бесіди…" "Наліпка" "Успіх" - "Запропоновано" "Пропозиції" "Синхронізація" + "Системна" "Текст" "Повідомлення третіх сторін" "Гілка" - "Гілки" "Тема" "Про що ця кімната?" "Неможливо розшифрувати" @@ -419,19 +385,14 @@ "Голосове повідомлення" "Очікування…" "Чекаємо на це повідомлення" - "Очікування геолокації…" "Будь-хто може переглянути історію" "Ви" - "%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 Ви впевнені, що хочете продовжити?" @@ -461,7 +422,6 @@ "%1$s не вдалося отримати доступ до вашого розташування. Повторіть спробу пізніше." "Не вдалося завантажити голосове повідомлення." "Кімната більше не існує або запрошення не чинне." - "Будь ласка, увімкніть GPS, щоб отримати доступ до функцій, що використовують геолокацію." "Повідомлення не знайдено" "%1$s не має дозволу на доступ до вашого розташування. Увімкнути доступ можна в Налаштуваннях." "%1$s не має дозволу на доступ до вашого розташування. Увімкніть доступ нижче." @@ -480,14 +440,6 @@ "Варіанти" "Вилучити %1$s" "Налаштування" - "Ніхто не ділиться своєю геопозицією" - "Обмін геопозицією" - - "%1$d особа" - "%1$d осіб" - "%1$d осіб" - - "На карті" "Не вдалося вибрати медіафайл, спробуйте ще раз." "Натисніть на повідомлення і виберіть \"%1$s\", щоб додати його сюди." "Закріпіть важливі повідомлення, щоб їх можна було легко знайти" @@ -513,7 +465,6 @@ "Повідомлення в %1$s" "Розгорнути" "Згорнути" - "Спільний доступ до місцезнаходження у реальному часі" "Уже переглядаєте цю кімнату!" "%1$s із %2$s" "%1$s закріплених повідомлень" @@ -526,14 +477,10 @@ "Відкрити в Картах 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 accba4556c..8e9c0fb3be 100644 --- a/libraries/ui-strings/src/main/res/values-ur/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ur/translations.xml @@ -141,6 +141,7 @@ "حقوقِ طبع و نشر" "کمرہ تخلیق کررہاہے…" "کمرہ چھوڑ لیا" + "اندھیرا" "رمزکشائی کی خرابی" "مطور اختیارات" "براہ راست گفتگو" @@ -167,6 +168,7 @@ "APK تنصیب کریں" "یہ میٹرکس شناخت نہیں مل سکتی، تو ہو سکتا ہے کہ دعوت نامہ موصول نہ ہو۔" "کمرہ چھوڑنا" + "روشنی" "ربط تختہ تراشہ پر نقل کردا گیا" "لاد رہا ہے…" @@ -210,6 +212,7 @@ "بازیابی کی کلید" "تاکہ کر رہا ہے…" "%1$s کا جواب دے رہے ہیں" + "ایک خطاء کی اطلاع دیں" "کسی مسئلے کی اطلاع دیں" "گزارش جمع ہوگئی" "امیر مدیرِ متن" @@ -238,6 +241,7 @@ "کامیابی" "تجاویز" "ہمسات سازی" + "نظام" "متن" "فریق ثالث کے اشعارات" "دھاگہ" 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 63e1756a23..aada6634d2 100644 --- a/libraries/ui-strings/src/main/res/values-uz/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uz/translations.xml @@ -1,7 +1,6 @@ "Reaksiya qoʻyish: %1$s" - "Manzil" "Avatar" "Xabar matni maydonini kichraytirish" "Oʻchirish" @@ -27,12 +26,9 @@ "Pauza" "Ovoz xabar, davomiyligi: %1$s, joriy holati: %2$s" "PIN-kod maydoni" - "Belgilangan joylashuv" "O\'ynang" - "Ijro tezligi" "So\'ro\'vnoma" "So‘rovnoma yakunlandi" - "QR kodi" "%1$s bilan munosabat bildiring" "Boshqa hisbelgilar bilan munosabat bildiring" "%1$s va %2$s bilan oʻqish" @@ -46,12 +42,9 @@ "%1$s bilan reaktsiyani olib tashlang" "Xona avatari" "Fayllarni yuborish" - "Yuboruvchining joylashuvi" "Amal bajarish vaqti cheklangan, tasdiqlash uchun bir daqiqa vaqtingiz bor" "Parolni ko\'rsatish" "Qoʻngʻiroqni boshlash" - "Video chaqiruvni boshlash" - "Ovozli qo‘ng‘iroq qilish" "Arxivlangan xona" "Foydalanuvchi avatari" "Foydalanuvchi menyusi" @@ -63,7 +56,6 @@ "Sizning avataringiz" "Qabul qiling" "Sarlavha qo\'shing" - "Mavjud xonalarni qo‘shish" "Vaqt jadvaliga qo\'shing" "Orqaga" "Qoʻngʻiroq" @@ -83,7 +75,6 @@ "Matnni nusxalash" "Yaratmoq" "Xonani yaratish" - "Maydon yaratish" "Faolsizlantirish" "Hisobni faolsizlantirish" "Rad etish" @@ -100,7 +91,6 @@ "Yoqish" "So‘rovnomani tugatish" "PIN kodni kiriting" - "Jamoat maydonlari o‘rganing" "Tugatish" "Parolni unutdingizmi?" "Oldinga" @@ -121,7 +111,6 @@ "Maydondan chiqish" "Ko\'proq yuklash" "Hisobni boshqarish" - "Hisob va qurilmalarni boshqarish" "Qurilmalarni boshqarish" "Xonalarni boshqarish" "Xabar" @@ -161,21 +150,18 @@ "Ovozli xabar yuborish" "Ulashish" "Havolani ulashing" - "Jonli joylashuvni ulashish" "Koʻrsatish" "Qaytadan kiring" - "Bu qurilmani olib tashlash" - "Bu qurilma baribir olib tashlansin" + "Tizimdan chiqish" + "Baribir tizimdan chiqing" "Oʻtkazib yuborish" "Boshlash" "Suhbatni boshlash" "Qaytadan boshlang" "Tasdiqlashni boshlang" "Xaritani yuklash uchun bosing" - "To‘xtatish" "Rasmga olmoq" "Variantlar uchun bosing" - "Tarjima" "Qayta urinib ko\'ring" "Olib tashlash" "Ko\'rish" @@ -205,11 +191,11 @@ "Buferga nusxa koʻchirildi" "Mualliflik huquqi" "Xona yaratilmoqda…" - "Joy yaratilmoqda…" "So\'rov bekor qilindi" "Xonani tark etdi" "Tar etilgan maydon" "Taklif rad etildi" + "Tungi" "Shifrni ochish xatosi" "Tavsif" "Dasturchi variantlari" @@ -225,7 +211,6 @@ "Bo\'sh fayl" "Shifrlash" "Shifrlash yoqilgan" - "Tugaydi: %1$s" "PIN kodini kiriting" "Xato" "Xato yuz berdi, siz yangi xabarlar uchun bildirishnomalarni olmasligingiz mumkin. Iltimos, sozlamalardan bildirishnomalarni bartaraf eting. @@ -235,7 +220,6 @@ Sababi:%1$s." "Xatolikka uchradi" "Sevimli" "Sevimli" - "Bildirishnomalar sinxronlanmoqda…" "Fayl" "Fayl o\'chirildi" "Fayl saqlandi" @@ -249,11 +233,10 @@ 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" - "Jonli joylashuv" - "Jonli joylashuv tugadi" "Yuklanmoqda…" "Batafsil yuklanmoqda…" @@ -280,7 +263,6 @@ Sababi:%1$s." "Oflayn" "Ochiq kodli litsenziyalar" "yoki" - "Boshqa variantlar" "Parol" "Odamlar" "Doimiy havola" @@ -298,10 +280,8 @@ Sababi:%1$s." "Tayyorlanmoqda…" "Maxfiylik siyosati" - "Maxfiy" "Shaxsiy xona" "Shaxsiy guruh" - "Ommaviy" "Jamoat xonasi" "Jamoat guruhi" "Reaktsiya" @@ -309,16 +289,15 @@ Sababi:%1$s." "Sabab" "Qayta tiklash kaliti" "Yangilanmoqda…" - "Olib tashlanmoqda…" "%1$d ta javob" "%1$d ta javob" "%1$sga Javob berilmoqda" + "Xato haqida xabar bering" "Muammo haqida xabar bering" "Hisobot topshirildi" "Boy matn muharriri" - "Rol" "Xona" "Xona nomi" "masalan, loyihangiz nomi" @@ -334,10 +313,6 @@ Sababi:%1$s." "Xavfsizlik" "Tomonidan koʻrilgan" "Hisobni tanlang" - - "%1$d ta tanlandi" - "%1$d ta tanlandi" - "Yubirish" "Yuborilmoqda…" "Yuborilmadi" @@ -348,15 +323,12 @@ Sababi:%1$s." "Server URL manzili" "Sozlamalar" "Maydonni ulashish" - "Yangi a’zolar tarixni ko‘radi" - "Ulashilgan jonli joylashuv" "Joylashuvi ulashildi" "Umumiy maydon" - "Qurilma olib tashlanmoqda" + "Chiqish" "Nimadir xato ketdi" "Muammoga duch keldik. Iltimos, qayta urinib koʻring." "Maydon" - "Maydon a’zolari" "Bu maydon nima haqida?" "%1$d Maydon" @@ -365,13 +337,12 @@ Sababi:%1$s." "Chat boshlanmoqda…" "Stiker" "Muvaffaqiyat" - "Tavsiya etilgan" "Tavsiyalar" "Sinxronlash" + "Tizim" "Matn" "Uchinchi tomon bildirishnomalari" "Ip" - "Mavzular" "Mavzu" "Bu xona nima haqida?" "Shifrni ochish imkonsiz" @@ -402,19 +373,14 @@ Sababi:%1$s." "Ovozli xabar" "Kutilmoqda…" "Ushbu xabarni kutilmoqda" - "Jonli joylashuv kutilmoqda…" - "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" + "%1$sning shaxsi qayta tiklandi.%2$s" + "%1$sʼning %2$s shaxsiy ma’lumotlari qayta tiklandi.%3$s" "(%1$s )" - "%1$s raqamli identifikatori asliga qaytarildi." - "%1$sning%2$s raqamli identifikatsiya qayta tiklandi.%3$s" + "%1$sning shaxsi qayta tiklandi." + "%1$sʼning %2$s shaxsiy ma’lumotlari qayta o‘rnatildi.%3$s" "Tasdiqlashni bekor qilish" - "Ruxsat berish" "%1$s havolasi sizni boshqa %2$s saytiga olib boradi Davom etasizmi?" @@ -444,7 +410,6 @@ Davom etasizmi?" "%1$sjoylashuvingizga kira olmadi. Iltimos keyinroq qayta urinib ko\'ring." "Ovozli xabaringizni yuklashda xatolik roʻy berdi." "Xona endi mavjud emas yoki taklif yaroqsiz." - "Joylashuvga asoslangan funksiyalardan foydalanish uchun GPS funksiyasini yoqing." "Xabar topilmadi" "%1$sjoylashuvingizga kirishga ruxsati yo\'q. Sozlamalar orqali kirishni yoqishingiz mumkin." "%1$sjoylashuvingizga kirishga ruxsati yo\'q. Quyida kirishni yoqing." @@ -463,7 +428,6 @@ Davom etasizmi?" "Parametrlar" "%1$sni olib tashlash" "Sozlamalar" - "Hech kim joylashuvini ulashmayapti" "Media tanlash jarayonida xatolik yuz berdi, qayta urinib ko\'ring" "Xabarni bosib, bu yerga kiritish uchun \"%1$s\"-ni tanlang." "Muhim xabarlarni osongina topish uchun qadang" @@ -472,11 +436,11 @@ Davom etasizmi?" "%1$d ta qadalgan xabar" "Qadalgan xabarlar" - "Raqamli identifikatoringizni tiklash uchun %1$s hisobingizga kirmoqchisiz. Shundan keyin ilovaga qaytarilasiz." - "Tasdiqlay olmayapsizmi? Raqamli identifikatorni tiklash uchun hisobingizga kiring." + "Shaxsingizni qayta o‘rnatish uchun %1$s hisobingizga kirishingiz kerak. Shundan so‘ng, avtomatik ravishda ilovaga qaytarilasiz." + "Tasdiqlanmadimi? Shaxsingizni tiklash uchun hisobingizga kiring." "Tasdiqlashni olib tashlang va yuboring" "Siz tasdiqlashni bekor qilib, bu xabarni baribir yuborishingiz yoki hozircha to‘xtatib, %1$sʼni qayta tasdiqlagandan so‘ng keyinroq yana urinib ko‘rishingiz mumkin." - "Xabaringiz yuborilmadi, chunki %1$sning tasdiqlangan raqamli identifikatori asliga qaytarildi" + "%1$sning tasdiqlangan shaxsiy ma’lumotlari qayta o‘rnatilganligi tufayli xabaringiz jo‘natilmadi" "Baribir xabar yuborilsin" "%1$s tasdiqlanmagan bir yoki bir nechta qurilmadan foydalanmoqda. Siz xabarni baribir yuborishingiz mumkin yoki hozircha bekor qilib, %2$s barcha qurilmalarini tasdiqlagunga qadar kutib, keyinroq qayta urinishingiz mumkin." "%1$s barcha qurilmalarni tasdiqlamagani uchun xabaringiz yuborilmadi" @@ -488,7 +452,6 @@ Davom etasizmi?" "Xabar %1$sda" "Kengaytirish" "Kamaytirish" - "Jonli joylashuvni ulashish" "Bu xona allaqachon ko‘rilmoqda!" "%1$sʼdan %2$s" "%1$s ta qadalgan xabar" @@ -501,15 +464,11 @@ Davom etasizmi?" "Google Mapsda oching" "OpenStreetMapda oching" "Bu joylashuvni ulashing" - "Ulashish parametrlari" "Siz yaratgan yoki qo‘shilgan maydonlar." "%1$s•%2$s" - "Xonalarni tartibga solish uchun maydon yarating" "%1$s ta maydon" "Maydonlar" - "%1$s ulashildi" - "Xaritada" - "Xabar yuborilmadi, chunki%1$s ning tasdiqlangan raqamli identifikatsiyasi qayta tiklandi." + "Xabar yuborilmadi, chunki %1$sʼning tasdiqlangan identifikatori asliga qaytarildi." "Xabar yuborilmadi, chunki %1$s barcha qurilmalarni tasdiqlamagan." "Xabaringiz yuborilmadi, chunki siz bir yoki bir nechta qurilmangizni tasdiqlamagan ekansiz." "Joylashuv" @@ -519,5 +478,5 @@ Davom etasizmi?" "Tarixiy xabarlarga kirish uchun bu qurilmani tasdiqlashingiz kerak" "Sizni ushbu xabarga ruxsatingiz yoʻq" "Xabarni shifrini ochib bo‘lmadi" - "Qurilmangizni tasdiqlamaganingiz yoki yuboruvchi raqamli shaxsingizni tasdiqlashi kerakligi sababli bu xabar bloklandi." + "Bu xabar bloklandi, chunki siz qurilmangizni tasdiqlamadingiz yoki yuboruvchi shaxsingizni tasdiqlashi kerak bo‘lgani sababli bloklandi" 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 5cfbe26fba..1cc07bb8cf 100644 --- a/libraries/ui-strings/src/main/res/values-vi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-vi/translations.xml @@ -6,7 +6,7 @@ "Thu nhỏ ô nhập tin nhắn" "Xóa" - "%1$d các chữ số đã nhập" + "Đã nhập %1$d chữ số" "Đổi ảnh đại diện" "Đường dẫn đầy đủ của phòng là %1$s" @@ -47,8 +47,7 @@ "Vị trí người gửi" "Yêu cầu hành động có giới hạn thời gian, bạn có một phút để xác minh" "Hiện mật khẩu" - "Bắt đầu cuộc gọi" - "Bắt đầu cuộc gọi video" + "Gọi" "Bắt đầu cuộc gọi thoại" "Phòng Tombstone" "Ảnh đại diện của người dùng" @@ -191,6 +190,7 @@ "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" @@ -208,6 +208,7 @@ "Đã 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" @@ -233,7 +234,6 @@ 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" @@ -247,6 +247,7 @@ 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" @@ -269,20 +270,13 @@ Lý do: %1$s ." "Tắt tiếng" "Tên" "Không có kết quả" - "Không có tên phòng" - "Không có tên space" "Không được mã hóa" "Ngoại tuyến" - "Giấy phép mã nguồn mở" "hoặc" - "Các lựa chọn khác" "Mật khẩu" "Danh bạ" "Liên kết cố định" "Quyền truy cập" - "Đã ghim" - "Vui lòng kiểm tra kết nối internet của bạn." - "Vui lòng chờ…" "Bạn có chắc chắn muốn kết thúc cuộc thăm dò này không?" "Khảo sát: %1$s" "Tổng số phiếu: %1$s" @@ -290,34 +284,20 @@ Lý do: %1$s ." "%d lượt bình chọn" - "Đang chuẩn bị…" "Chính sách bảo mật" - "Riêng tư" "Phòng riêng tư" - "Không gian riêng tư" - "Công cộng" - "Phòng công cộng" - "Không gian công cộng" "Biểu cảm" "Cảm xúc" - "Lý do" "Khóa khôi phục." "Đang làm mới…" - "Đang xóa…" - - "%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" - "Vai trò" "Phòng" "Tên phòng" "ví dụ: tên dự án của bạn" - - "%1$d Phòng" - "Đã lưu thay đổi" "Đang lưu" "Khóa màn hình" @@ -325,11 +305,6 @@ Lý do: %1$s ." "Kết quả tìm kiếm" "Bảo mật" "Được xem bởi" - "Chọn tài khoản" - - "%1$d đã chọn" - - "Gửi đến" "Đang gửi…" "Không gửi được" "Đã gửi" @@ -348,15 +323,13 @@ Lý do: %1$s ." "Không gian" "Thành viên không gian" "Không gian này dùng để làm gì?" - - "%1$d Không gian" - "Đang bắt đầu cuộc trò chuyện…" "Sticker" "Thành công" "Gợi ý" "Gợi ý" "Đang đồng bộ" + "Hệ thống" "Văn bản" "Thông báo từ bên thứ ba" "Chủ đề" @@ -364,9 +337,7 @@ Lý do: %1$s ." "Chủ đề" "Phòng này dùng để làm gì?" "Không thể giải mã" - "Được gửi từ một thiết bị không an toàn" "Bạn không thể xem tin nhắn này" - "Danh tính kỹ thuật số đã được xác minh của người gửi đã được đặt lại." "Không thể gửi lời mời đến một hoặc nhiều người dùng." "Không thể gửi lời mời" "Mở khóa" @@ -412,7 +383,6 @@ Bạn có chắc muốn tiếp tục không?" "Kích thước tệp tối đa cho phép là: %1$s" "Kích thước tệp quá lớn để tải lên" "Phòng đã được báo cáo" - "Đã báo cáo và rời khỏi phòng." "Xác nhận" "Lỗi" "Thành công" @@ -423,23 +393,14 @@ Bạn có chắc muốn tiếp tục không?" "Kích thước tệp tối đa cho phép là: %1$s" "Chọn chất lượng video bạn muốn tải lên." "Chọn chất lượng tải lên video" - "Tìm kiếm biểu tượng cảm xúc" - "Bạn đã đăng nhập trên thiết bị này với tư cách là%1$s ." - "Máy chủ của bạn cần được nâng cấp để hỗ trợ Dịch vụ Xác thực và tạo tài khoản." "Không tạo được liên kết cố định" "%1$s không thể tải bản đồ. Vui lòng thử lại sau." "Không tải được tin nhắn" "%1$s không thể truy cập vị trí của bạn. Vui lòng thử lại sau." "Không thể tải lên tin nhắn thoại của bạn." - "Phòng đó không còn tồn tại hoặc lời mời không còn hiệu lực." - "Vui lòng bật GPS để truy cập các tính năng dựa trên vị trí." - "Không tìm thấy tin nhắn" "%1$s không có quyền truy cập vị trí của bạn. Bạn có thể bật quyền trong Cài đặt." "%1$s chưa được phép truy cập vị trí. Bật quyền dưới đây." "%1$s không có quyền truy cập micro của bạn. Hãy bật quyền để ghi tin nhắn thoại." - "Nguyên nhân có thể là do sự cố mạng hoặc máy chủ." - "Địa chỉ phòng này đã tồn tại. Vui lòng thử chỉnh sửa trường địa chỉ phòng hoặc thay đổi tên phòng." - "Một số ký tự không được phép. Chỉ các chữ cái, chữ số và các ký hiệu sau được hỗ trợ: ! $ &amp; ' ( ) * + / ; = ? @ [ ] - . _" "Một số tin nhắn chưa được gửi" "Rất tiếc, đã có lỗi xảy ra." "🔐️ Tham gia cùng tôi trên %1$s" @@ -447,9 +408,6 @@ Bạn có chắc muốn tiếp tục không?" "%1$s Android" "Lắc điện thoại để báo cáo lỗi" "Không thể chọn tệp phương tiện. Vui lòng thử lại." - - "%1$d tin nhắn được ghim" - "Tin nhắn được ghim" "Xử lý phương tiện tải lên không thành công, vui lòng thử lại." "Không thể lấy thông tin người dùng" 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 71159b78e4..2b12c212a7 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 @@ -1,7 +1,6 @@ "新增反應:%1$s" - "地址" "大頭貼" "最小化訊息文字欄位" "刪除" @@ -26,12 +25,9 @@ "暫停" "語音訊息,時長:%1$s,目前位置:%2$s" "PIN 碼欄位" - "固定位置" "播放" - "播放速度" "投票" "投票已結束" - "QR Code" "使用 %1$s 回應" "用其他表情符號回應" "%1$s 和 %2$s 已讀" @@ -44,12 +40,9 @@ "移除反應 %1$s" "聊天室大頭照" "傳送檔案" - "傳送者位置" "需要限時動作,您有一分鐘可以驗證" "顯示密碼" "開始通話" - "開始視訊通話" - "開始語音通話" "墓碑聊天室" "使用者大頭照" "使用者選單" @@ -61,7 +54,6 @@ "您的大頭照" "接受" "新增標題" - "新增既有聊天室" "新增至時間軸" "返回" "通話" @@ -81,7 +73,6 @@ "複製文字" "建立" "建立聊天室" - "建立空間" "停用" "停用帳號" "拒絕" @@ -98,7 +89,6 @@ "啟用" "結束投票" "輸入 PIN 碼" - "探索公開空間" "結束" "忘記密碼?" "轉寄" @@ -119,7 +109,6 @@ "離開空間" "載入更多" "管理帳號" - "管理帳號與裝置" "管理裝置" "管理聊天室" "聊天" @@ -159,18 +148,16 @@ "傳送語音訊息" "分享" "分享連結" - "分享即時位置" "顯示" "再登入一次" - "移除此裝置" - "仍要移除此裝置" + "登出" + "直接登出" "略過" "開始" "開始聊天" "重新開始" "開始驗證" "點擊以載入地圖" - "停止" "拍照" "點擊以查看選項" "翻譯" @@ -203,11 +190,11 @@ "已複製到剪貼簿" "著作權" "正在建立聊天室…" - "正在建立空間……" "請求已取消" "已離開聊天室" "離開空間" "邀請被拒絕" + "深色" "解密錯誤" "描述" "開發者選項" @@ -223,7 +210,6 @@ "空檔案" "加密" "已啟用加密" - "結束於 %1$s" "輸入您的 PIN 碼" "錯誤" "發生錯誤,您可能無法收到新訊息的通知。請從設定中進行通知疑難排解。 @@ -233,7 +219,6 @@ "失敗" "我的最愛" "我的最愛" - "正在同步通知……" "檔案" "檔案已刪除" "檔案已儲存" @@ -247,11 +232,9 @@ "找不到此 Matrix ID,因此可能沒有人會收到邀請。" "正在離開聊天室" "離開空間" + "淺色" "行已複製到剪貼簿" "連結已複製到剪貼簿" - "連結新裝置" - "即時位置" - "即時位置已結束" "載入中…" "載入更多……" @@ -262,12 +245,10 @@ "訊息" "訊息動作" - "訊息傳送失敗" "訊息佈局" "訊息已移除" "現代" "關閉通知" - "名稱" "%1$s (%2$s)" "查無結果" "無聊天室名稱" @@ -276,7 +257,6 @@ "離線" "開放原始碼授權條款" "或" - "其他選項" "密碼" "夥伴" "永久連結" @@ -293,10 +273,8 @@ "正在準備……" "隱私權政策" - "私人" "私密聊天室" "私人空間" - "公開" "公開的聊天室" "公開空間" "回應" @@ -304,18 +282,17 @@ "理由" "復原金鑰" "重新整理中…" - "正在移除……" "%1$d 個回覆" "正在回覆%1$s" + "回報程式錯誤" "回報問題" "已遞交報告" "格式化文字編輯器" - "角色" "聊天室" "聊天室名稱" - "範例:您的專案名稱" + "範例:您的計畫名稱" "%1$d 個聊天室" @@ -327,9 +304,6 @@ "安全性" "已讀" "選取帳號" - - "已選取 %1$d 個" - "傳送給" "傳送中…" "傳送失敗" @@ -340,35 +314,30 @@ "伺服器 URL" "設定" "分享空間" - "新成員可以檢視歷史" - "分享即時位置" "位置分享" "共享空間" - "正在移除裝置" + "正在登出" "有錯誤發生" "我們了遇到了問題。請再試一次。" "空間" - "空間成員" - "此空間的用途是?" "%1$d 個空間" "開始聊天…" "貼圖" "成功" - "已建議" "建議" "同步中" + "系統" "文字" "第三方通知" "討論串" - "討論串" "主題" - "此聊天室的用途是?" + "這個聊天室是做什麼用的?" "無法解密" "從不安全的裝置傳送" "您無法存取此則訊息" - "傳送者的驗證數位身份已重設" + "傳送者的驗證身份已重設" "無法發送邀請給一或多個使用者。" "無法發送邀請" "解鎖" @@ -393,19 +362,13 @@ "語音訊息" "等待中…" "等待此則訊息" - "正在等待即時位置……" - "任何人都可以檢視歷史" "您" - "因為您當時不在聊天室內,所以 %1$s (%2$s) 分享了此訊息。" - "因為您當時不在聊天室裡面,因此 %1$s 分享了此訊息。" - "此聊天室被設定為方便新成員閱讀歷史紀錄。%1$s" - "%1$s 的數位身份似乎已重設。%2$s" - "%1$s 的 %2$s 數位身份似乎已重設。%3$s" + "%1$s 的身份似乎已重設。%2$s" + "%1$s 的 %2$s 身份似乎已重設。%3$s" "(%1$s)" - "%1$s 的數位身份被重設。" - "%1$s 的 %2$s 數位身份已重設。%3$s" + "%1$s 的已驗證身份被重設。" + "%1$s 的 %2$s 驗證身份已重設。 %3$s" "撤回驗證" - "允許存取" "連結 %1$s 會將您帶往其他網站 %2$s 您確定您想要繼續嗎?" @@ -435,7 +398,6 @@ "%1$s 無法取得您的位置。請稍後再試。" "無法上傳語音訊息。" "此聊天室不再存在或邀請不再有效。" - "請啟用您的 GPS 以存取以位置為基礎的功能。" "找不到訊息" "%1$s 沒有權限存取您的位置。您可以到設定中開啟權限。" "%1$s 沒有權限存取您的位置。請在下方開啟權限。" @@ -461,11 +423,11 @@ "%1$d 則釘選的訊息" "釘選訊息" - "您將要前往您的 %1$s 帳號重設數位身份。然後您將會被帶回應用程式。" - "無法確認?前往您的帳號以重設您的數位身份。" + "您將要前往您的 %1$s 帳號重設身份。然後您將會被帶回應用程式。" + "無法確認?前往您的帳號以重設您的身份。" "撤回驗證並傳送" "您可以撤回您的驗證並仍傳送此訊息,或者您也可以立刻取消並在重新驗證 %1$s 後再試一次。" - "因為 %1$s 的驗證數位身份已重設,因此未傳送您的訊息。" + "因為 %1$s 的驗證身份已重設,因此未傳送您的訊息。" "仍要傳送訊息" "%1$s 正在使用一個或多個未經驗證的裝置。您仍然可以傳送訊息,也可以立刻取消並在 %2$s 驗證其所有裝置後再試一次。" "未傳送您的訊息,因為 %1$s 尚未驗證所有裝置。" @@ -477,7 +439,6 @@ "%1$s 中的訊息" "展開" "減少" - "分享即時位置" "已檢視此聊天室!" "第 %1$s 個,共 %2$s 個" "%1$s 個釘選訊息" @@ -489,16 +450,12 @@ "在 Apple Maps 中開啟" "在 Google Maps 中開啟" "在開放街圖(OpenStreetMap) 中開啟" - "分享選定的位置" - "分享選項" + "分享這個位置" "您建立或加入的空間" "%1$s • %2$s" - "建立空間以整理聊天室" "%1$s 空間" "空間" - "已分享 %1$s" - "在地圖上" - "因為 %1$s 的驗證數位身份已重設,因此未傳送訊息。" + "因為 %1$s 的驗證身份已重設,因此未傳送訊息。" "訊息未傳送,因為 %1$s 尚未驗證所有裝置。" "因為您尚未驗證一個或多個裝置,因此未傳送訊息" "位置" @@ -508,5 +465,5 @@ "您必須驗證此裝置才能存取歷史訊息" "您無法存取此則訊息" "無法解密訊息" - "此訊息被封鎖,原因可能是您尚未驗證裝置,或是寄件者需要驗證您的數位身分。" + "此訊息被封鎖是因為您沒有驗證您的裝置,或是因為傳送者需要驗證您的身份而被封鎖。" 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 1b50794182..1ce3a75ced 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -1,6 +1,6 @@ - "添加反应:%1$s" + "添加表情符号:%1$s" "地址" "头像" "最小化消息文本框" @@ -8,16 +8,13 @@ "已输入 %1$d 个数字" - "持续时间:%1$s" "编辑头像" "完整地址为 %1$s" "加密详情" "展开消息文本框" "隐藏密码" - "信息" "加入通话" "跳转到底部" - "跳转到未读" "将地图移动到我的位置" "仅提及" "通知已关闭" @@ -34,30 +31,25 @@ "播放速度" "投票" "投票已结束" - "位置:%1$s" "二维码" - "使用 %1$s 反应" - "使用其它 Emoji 做出反应" + "使用 %1$s 回应" + "使用其他表情符号回应" "%1$s 和 %2$s 已读" "%1$s 及其他 %2$d 人已读" "%1$s 已读" "点击以显示全部" - "移除反应:%1$s" - "移除反应:%1$s" + "撤回反应 %1$s" + "移除表情符号%1$s" "房间头像" "发送文件" "发送方位置" - "由 %1$s 发送于 %2$s" - "请求的操作有时间限制,你有 1 分钟的时间来验证" + "限时操作,您有一分钟的时间来验证" "显示密码" "开始通话" - "开始视频通话" - "开始语音通话" - "位于 %1$s 中的消息列" - "位于 %1$s 中的消息列" - "已封存的房间" + "发起语音通话" + "已封存的聊天室" "用户头像" "用户菜单" "查看头像" @@ -65,16 +57,15 @@ "语音消息,时长:%1$s" "录制语音消息" "停止录制" - "你的头像" + "您的头像" "接受" "添加标题" - "添加现有房间" + "添加现有聊天室" "添加到时间线" "返回" - "通话" + "呼叫" "取消" "暂时取消" - "选择文件" "选择照片" "清除" "关闭" @@ -88,21 +79,18 @@ "复制消息链接" "复制文本" "创建" - "创建房间" + "创建聊天室" "创建空间" "停用" "停用账户" "拒绝" "拒绝并屏蔽" - "删除" - "删除账户" "删除投票" "取消全选" "禁用" "丢弃" "关闭" "完成" - "下载" "编辑" "编辑标题" "编辑投票" @@ -118,34 +106,33 @@ "前往设置" "忽略" "邀请" - "邀请人员" - "邀请人员加入 %1$s" - "邀请人员加入 %1$s" + "邀请朋友" + "邀请别人加入 %1$s" + "邀请别人加入 %1$s" "邀请" "加入" "了解更多" "离开" "离开聊天" - "离开房间" + "离开聊天室" "离开空间" "载入更多" "管理账户" - "管理账户与设备" "管理设备" - "管理房间" - "发送消息" + "管理聊天室" + "发送消息给" "最小化" "下一步" "否" - "暂不" + "以后再说" "确定" "打开上下文菜单" - "设置" - "使用其它方式打开" + "打开设置" + "用其他方式打开" "置顶" "快速回复" "引用" - "反应" + "回应" "拒绝" "移除" "删除标题" @@ -153,7 +140,7 @@ "回复" "在消息列中回复" "举报" - "报告 bug" + "报告错误" "举报内容" "举报对话" "举报房间" @@ -173,54 +160,52 @@ "共享实时位置" "显示" "再次登录" - "移除此设备" - "仍要移除此设备" + "删除此设备" + "仍要删除此设备" "跳过" "开始" "开始聊天" "重新开始" "开始验证" "点击以加载地图" - "停止" "拍摄照片" "点按查看选项" "翻译" - "重试" + "再试一次" "取消置顶" "查看" - "在时间线上查看" + "在时间轴中查看" "查看源码" "是" - "是,重试" - "你的服务器现在支持更快的新协议。现在注销并重新登录以升级。立即这样做可以避免你在以后删除旧协议时被强制注销。" + "是的,再试一次" + "您的服务器现在支持更快的新协议。现在登出并重新登录以进行升级。现在这样做可以帮助您避免在以后删除旧协议时被强制登出。" "有可用升级" "关于" "可接受的使用政策" "添加账户" - "添加账户" - "正在添加标题" + "添加另一个账户" + "添加标题" "高级设置" "一张图片" "分析" - "你离开了房间" - "你已注销会话" + "你离开了聊天室" + "您已退出会话" "外观" "音频" - "Beta" + "测试版" "已屏蔽用户" "气泡" - "来电被拒接" "通话已开始" - "你已拒接来电" "聊天记录备份" "已复制到剪贴板" "版权" - "正在创建房间…" - "正在创建空间…" - "申请已取消" - "离开房间" + "正在创建聊天室…" + "正在创建空间……" + "请求已取消" + "离开聊天室" "离开空间" "邀请已拒绝" + "深色" "解密错误" "描述" "开发者选项" @@ -230,13 +215,12 @@ "下载失败" "正在下载" "(已编辑)" - "正在编辑" - "正在编辑标题" + "编辑中" + "编辑标题" "* %1$s %2$s" "空文件" "加密" "已启用加密" - "于 %1$s 结束" "输入 PIN 码" "错误" "发生错误,可能无法收到新消息通知。请在设置中对通知进行故障排除。 @@ -246,7 +230,6 @@ "失败" "收藏" "已收藏" - "正在同步通知…" "文件" "文件已删除" "文件已保存" @@ -258,20 +241,19 @@ "回复 %1$s" "安装 APK" "找不到此 Matrix ID,因此可能无法收到邀请。" - "正在离开房间" + "正在离开聊天室" "正在离开空间" + "浅色" "链接已复制到剪贴板" "链接已复制到剪贴板" "关联新设备" - "实时位置" - "实时位置已结束" "正在加载…" - "正在加载更多…" + "正在加载更多……" "其他 %d 人" - "%1$d 个成员" + "%1$d个成员" "消息" "消息操作" @@ -283,20 +265,20 @@ "名称" "%1$s (%2$s)" "没有结果" - "无房间名称" + "无聊天室名" "未命名空间" "未加密" "离线" "开源许可证" "或" - "其它选项" + "其他选项" "密码" - "人员" - "永久链接" + "用户" + "固定链接" "权限" "已置顶" "请检查 Internet 连接" - "请稍候…" + "请稍候……" "确定要结束这个投票吗?" "投票:%1$s" "总票数:%1$s" @@ -307,13 +289,13 @@ "正在准备…" "隐私政策" "私密" - "私有房间" + "私有聊天室" "私有空间" "公共" - "公共房间" - "公共空间" - "反应" - "反应" + "公共聊天室" + "公开空间" + "回应" + "回应" "理由" "恢复密钥" "正在刷新…" @@ -322,15 +304,16 @@ "%1$d 个回复" "正在回复 %1$s" + "报告错误" "报告问题" "报告已提交" "富文本编辑器" "角色" - "房间" - "房间名称" - "例如:你的项目名称" + "聊天室" + "聊天室名称" + "例如:您的项目名称" - "%1$d 个房间" + "%1$d 房间" "保存的更改" "正在保存" @@ -341,7 +324,7 @@ "已读" "选择账户" - "已选中 %1$d 个" + "%1$d 已选中" "发送至" "正在发送…" @@ -353,31 +336,30 @@ "服务器 URL" "设置" "共享空间" - "新成员可以看到历史" - "共享实时位置" + "新成员可见历史记录" "共享位置" - "已共享的空间" - "正在移除设备" + "共享空间" + "正在登出" "发生了一些错误" "我们遇到了一个问题。请重试。" "空间" "空间成员" "该空间的主题是什么?" - "%1$d 个空间" + "%1$d 空间" "开始聊天…" "贴纸" "成功" - "建议" + "推荐" "建议" "正在同步" + "系统" "文本" "第三方通知" "消息列" - "消息列" "主题" - "该房间的主题是什么?" + "该聊天室的主题是什么?" "无法解密" "从不安全的设备发送" "无权访问此消息" @@ -386,7 +368,7 @@ "无法发送邀请" "解锁" "解除静音" - "不受支持的通话" + "不支持的呼叫" "不支持的事件" "用户名" "验证已取消" @@ -404,135 +386,121 @@ "标准质量" "质量与上传速度的平衡" "语音消息" - "正在等待…" - "正在等待此消息" - "正在等待实时位置…" - "任何人都可以看到历史" - "你" - "由于你当时不在房间内,%1$s(%2$s)已将消息向你共享。" - "由于你当时不在房间内,%1$s 已将消息向你共享。" - "此房间已配置为允许新成员阅读历史。%1$s" - "%1$s 的数字身份已重置。%2$s" - "%1$s(%2$s)的数字身份已重置。%3$s" + "等待…" + "正在等待解密密钥" + "任何人都可查看历史记录" + "您" + "%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 的数字身份已重置。%3$s" "撤回验证" "允许访问" "链接 %1$s 将跳转至外部网站 %2$s 确定要继续吗?" "请再次确认链接" - "选择你上传的视频的默认质量。" + "选择您上传的视频的默认质量。" "视频上传质量" "允许的最大文件大小为:%1$s" "文件太大,无法上传" "已举报房间" - "已举报并离开房间" + "举报并离开房间" "确认" "错误" "成功" "警告" - "你有未保存的更改。" + "您有未保存的更改。" "更改尚未保存,确定要返回吗?" "保存更改?" "允许的最大文件大小为:%1$s" - "选择你要上传的视频的质量。" + "选择您要上传的视频的质量。" "选择视频上传质量" - "搜索 Emoji" - "你已在此设备以 %1$s 的身份登录。" - "你的主服务器需要升级,以支持 Matrix 认证服务和账户创建。" - "永久链接创建失败" + "搜索表情符号" + "您已在此设备以%1$s 身份登录。" + "您的服务器需要升级,以支持 Matrix 鉴权服务和账户创建。" + "创建固定链接失败" "%1$s 无法加载地图,请稍后再试。" - "消息加载失败" - "%1$s 无法访问你的位置,请稍后再试。" + "加载消息失败" + "%1$s 无法访问您的位置,请稍后再试。" "无法上传语音消息。" "该房间已不存在或邀请已失效。" "请开启 GPS 以使用基于位置的功能。" - "未找到消息" - "%1$s 无权访问你的位置。你可以在“设置”中启用位置权限。" - "%1$s 无权访问你的位置。在下方启用访问权限。" - "%1$s 无权访问你的麦克风。启用访问权以录制语音消息。" + "找不到消息" + "%1$s 没有权限访问您的位置。您可以在设置中启用位置权限。" + "%1$s 没有权限访问您的位置。在下方启用位置权限。" + "%1$s 没有权限访问您的麦克风。启用录制语音消息的权限。" "这可能是由于网络或服务器问题导致" "此房间地址已存在。请尝试编辑房间地址字段或更改房间名称" "不允许使用某些字符。仅支持字母、数字和以下符号 $ & ‘ ( ) * + / ; = ? @ [ ] - . _" - "某些消息尚未发送" + "某些信息尚未发送" "抱歉,发生了错误" - "🔐️ 在 %1$s 中与我一起" + "🔐️ 加入我 %1$s" "嗨!请通过 %1$s 与我联系:%2$s" - "正在共享实时位置" - "位置共享正在进行" "%1$s Android" - "摇一摇以报告 bug" + "摇一摇以报错" "屏幕截图" "%1$s:%2$s" "选项" - "移除 %1$s" + "移除%1$s" "设置" - "向左旋转图像" - - "%1$d 度" - - "编辑照片" - "目前无人分享其位置" - "共享实时位置" - - "%1$d 个人" - - "在地图上" "选择媒体失败,请重试。" + "欢迎回来" "按下消息并选择 “%1$s” 将其包含在此处。" - "置顶重要的消息以便于发现" + "固定重要消息,以便轻松发现它们" - "%1$d 个已置顶的消息" + "%1$d 置顶消息" - "已置顶的消息" - "你即将被重定向到你在 %1$s 上的账户以重置数字身份。之后将被带回 app。" - "无法确认?请转到你的账户重置数字身份。" + "置顶消息" + "您将要转到您的%1$s帐户来重置您的数字身份。之后,您将被带回该应用。" + "无法确认?请前往您的帐户重置您的数字身份。" "撤回验证并发送" - "你可以撤回验证并照常发送此消息,也可以暂时取消验证,并于重新验证 %1$s 后重试。" - "你的消息未能发送,因为 %1$s 的已验证数字身份已被重置" + "您可以撤回验证并仍然发送此消息;也可以暂时取消验证,在重新验证 %1$s 后重试。" + "您的消息未发送,因为%1$s的已验证数字身份已被重置" "仍然发送消息" - "%1$s 正在使用至少 1 个未经验证的设备。你可以照常发送消息,也可以暂时取消,直到 %2$s 验证所有设备后重试。" - "你的消息未能发送,因为 %1$s 尚未验证所有设备" - "你有至少 1 个未经验证的设备。你可以照常发送消息,也可以暂时取消,并在验证所有设备后重试。" - "你的消息未能发送,因为你有尚未验证的设备。" + "%1$s 正在使用一个或多个未经验证的设备。您还是可以继续发送信息;也可以暂时取消,等 %2$s 验证了所有设备后重试。" + "您的消息未发送,因为%1$s尚未验证所有设备" + "您有未验证的设备。您仍然可以发送消息;也可以暂时取消,并在验证所有设备后稍后重试。" + "您的消息未发送,因为您有尚未验证的设备。" "编辑管理员或所有者" "处理要上传的媒体失败,请重试。" "无法获取用户信息" "%1$s 中的消息" "展开" "折叠" - "共享实时位置" - "已经位于此房间!" + "已经在此房间了!" "%1$s / %2$s" - "%1$s 个已置顶的消息" + "置顶消息 %1$s" "正在加载消息…" "查看全部" "聊天" - "共享位置" - "共享我的位置" + "分享位置" + "分享我的位置" "在 Apple Maps 中打开" "在 Google Maps 中打开" "在 OpenStreetMap 中打开" "分享选定的位置" "共享选项" - "你创建或加入的空间。" + "您创建或加入的空间。" "%1$s • %2$s" - "创建空间以组织房间" - "空间 %1$s" + "创建空间以组织聊天室" + "%1$s空间" "空间" - "已共享 %1$s" + "共享于%1$s" "在地图上" - "消息未能发送,因为 %1$s 的已验证数字身份已被重置。" - "消息未能发送,因为 %1$s 尚未验证所有设备。" - "消息未能发送,因为你有尚未验证的设备。" + "消息未发送,因为%1$s的已验证数字身份已被重置。" + "消息未发送,因为%1$s尚未验证所有设备。" + "消息未发送,因为您有尚未验证的设备。" "位置" - "版本:%1$s(%2$s)" + "版本:%1$s (%2$s)" "zh-Hans" "历史消息在此设备上不可用" - "你需要验证此设备才能访问历史消息" + "您需要验证此设备才能访问历史消息" "无权访问此消息" "无法解密消息" - "此消息已被阻止,因为你未验证你的设备,或发送者需要验证你的数字身份。" + "此消息已被阻止,因为您未验证您的设备,或者发件人需要验证您的身份。" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 0b9490ae59..f91e3a85b0 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -9,16 +9,13 @@ "%1$d digit entered" "%1$d digits entered" - "Duration: %1$s" "Edit avatar" "The full address will be %1$s" "Encryption details" "Expand message text field" "Hide password" - "Info" "Join call" "Jump to bottom" - "Jump to unread" "Move the map to my location" "Mentions only" "Muted" @@ -35,7 +32,6 @@ "Playback speed" "Poll" "Ended poll" - "Position: %1$s" "QR Code" "React with %1$s" "React with other emojis" @@ -51,15 +47,11 @@ "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" @@ -77,7 +69,6 @@ "Call" "Cancel" "Cancel for now" - "Choose file" "Choose photo" "Clear" "Close" @@ -97,15 +88,12 @@ "Deactivate account" "Decline" "Decline and block" - "Delete" - "Delete account" "Delete Poll" "Deselect all" "Disable" "Discard" "Dismiss" "Done" - "Download" "Edit" "Edit caption" "Edit poll" @@ -200,11 +188,12 @@ "About" "Acceptable use policy" "Add an account" - "Add account" + "Add another account" "Adding caption" "Advanced settings" "an image" "Analytics" + "Syncing notifications…" "You left the room" "You were logged out of the session" "Appearance" @@ -212,9 +201,7 @@ "Beta" "Blocked users" "Bubbles" - "Call declined" "Call started" - "You declined a call" "Chat backup" "Copied to clipboard" "Copyright" @@ -224,6 +211,7 @@ "Left room" "Left space" "Invite declined" + "Dark" "Decryption error" "Description" "Developer options" @@ -249,7 +237,6 @@ Reason: %1$s." "Failed" "Favourite" "Favourited" - "Syncing notifications…" "File" "File deleted" "File saved" @@ -263,6 +250,7 @@ Reason: %1$s." "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" @@ -304,7 +292,6 @@ 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" @@ -330,6 +317,7 @@ Reason: %1$s." "%1$d replies" "Replying to %1$s" + "Report a bug" "Report a problem" "Report submitted" "Rich text editor" @@ -383,6 +371,7 @@ Reason: %1$s." "Suggested" "Suggestions" "Syncing" + "System" "Text" "Third-party notices" "Thread" @@ -471,9 +460,6 @@ 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" @@ -481,20 +467,16 @@ Are you sure you want to continue?" "Options" "Remove %1$s" "Settings" - "Rotate the image to the left" - - "%1$d degree" - "%1$d degrees" - - "Edit photo" - "Nobody is sharing their location" - "Sharing live location" - - "%1$d person" - "%1$d people" - - "On the map" "Failed selecting media, please try again." + "Open Element Classic" + "Open Element Classic on your device" + "Go to Settings > Security & Privacy" + "In Cryptography keys management, select Encrypted messages recovery" + "Follow the instructions to enable your key storage" + "Come back to %1$s" + "Enable your key storage before proceeding to %1$s" + "Checking account" + "Welcome back" "Press on a message and choose “%1$s” to include here." "Pin important messages so that they can be easily discovered" 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 deleted file mode 100644 index ed35cfbc3e..0000000000 --- a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/a11y/HasExternalKeyboard.kt +++ /dev/null @@ -1,49 +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.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/strings/Plurals.kt b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/strings/Plurals.kt deleted file mode 100644 index fe927e725d..0000000000 --- a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/strings/Plurals.kt +++ /dev/null @@ -1,37 +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.ui.utils.strings - -import androidx.annotation.StringRes -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.res.stringResource - -/** - * Similar to [androidx.compose.ui.res.pluralStringResource] but with separate resource ids for singular and plural values. - * Useful when we want to use different strings for singular and plural forms but not mentioning the actual quantity in the string. - * In this case, we cannot use getQuantityString, because some locales have more than two plural forms, and require the quantity to - * be part of the resulting strings. - * @param resIdForOne Resource id for the case when [count] is 1. - * @param resIdForOthers Resource id for the other cases ([count] is not 1). - * @param count The quantity to determine whether to use singular or plural form. Must be greater than or equal to 1. - * @param formatArgs The format arguments that will be used for substitution in the resulting string. Will be applied to either - * the singular or plural string depending on the quantity. - * @return The localized string corresponding to the given quantity. - */ -@Composable -@ReadOnlyComposable -fun simplePluralStringResource( - @StringRes resIdForOne: Int, - @StringRes resIdForOthers: Int, - count: Int, - vararg formatArgs: Any, -): String { - val resId = if (count == 1) resIdForOne else resIdForOthers - return stringResource(resId, *formatArgs) -} diff --git a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/a11y/IsTalkbackEnabled.kt b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/IsTalkbackEnabled.kt similarity index 96% rename from libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/a11y/IsTalkbackEnabled.kt rename to libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/IsTalkbackEnabled.kt index 938a774355..60ac1887c6 100644 --- a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/a11y/IsTalkbackEnabled.kt +++ b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/IsTalkbackEnabled.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.ui.utils.a11y +package io.element.android.libraries.ui.utils.time 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 e058210b7d..f37c263d83 100644 --- a/libraries/voiceplayer/api/build.gradle.kts +++ b/libraries/voiceplayer/api/build.gradle.kts @@ -16,6 +16,5 @@ 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 8fe79fb774..4aa00e188b 100644 --- a/libraries/voiceplayer/impl/build.gradle.kts +++ b/libraries/voiceplayer/impl/build.gradle.kts @@ -21,7 +21,6 @@ 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 9d41b0da92..9d471f7a6d 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,6 +9,7 @@ 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 @@ -19,7 +20,7 @@ import java.io.File */ @ContributesBinding(RoomScope::class) class DefaultEncoder( - private val encoderProvider: () -> OggOpusEncoder, + private val encoderProvider: Provider, config: AudioConfig, ) : Encoder { private val bitRate = config.bitRate 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 4c1c476c7a..134a9bcdb5 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,5 +14,4 @@ data class ElementWellKnown( val rageshakeUrl: String?, val brandColor: String?, val notificationSound: String?, - val identityProviderAppScheme: String?, ) diff --git a/libraries/wellknown/impl/build.gradle.kts b/libraries/wellknown/impl/build.gradle.kts index 1e2c4d7d61..f803eeec3c 100644 --- a/libraries/wellknown/impl/build.gradle.kts +++ b/libraries/wellknown/impl/build.gradle.kts @@ -33,13 +33,9 @@ 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 a0223e93cc..3bcf9bf573 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,70 +10,29 @@ 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) - val parsed = json().decodeFromString(data).map() - // Also store in cache, if valid - cacheStore.storeData( - key = url, - data = CacheData( - value = data, - updatedAt = systemClock.epochMillis(), - ) - ) - parsed + json().decodeFromString(data).map() } .toWellknownRetrieverResult() } @@ -92,9 +51,4 @@ 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 2e2b5de16f..d4661d1be0 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 @@ -32,6 +32,4 @@ data class InternalElementWellKnown( val brandColor: String? = null, @SerialName("notification_sound") val notificationSound: String? = null, - @SerialName("idp_app_scheme") - val identityProviderAppScheme: String? = 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 41ed54d7db..c7ca088e68 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 @@ -16,5 +16,4 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown( rageshakeUrl = rageshakeUrl, brandColor = brandColor, notificationSound = notificationSound, - identityProviderAppScheme = identityProviderAppScheme, ) 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 cca40e283f..faad139a36 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,30 +6,16 @@ * 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.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 @@ -50,7 +36,6 @@ class DefaultSessionWellknownRetrieverTest { rageshakeUrl = null, brandColor = null, notificationSound = null, - identityProviderAppScheme = null, ) ) ) @@ -63,7 +48,13 @@ class DefaultSessionWellknownRetrieverTest { val sut = createDefaultSessionWellknownRetriever( getUrlLambda = { Result.success( - WELLKNOWN_CONTENT.toByteArray() + """{ + "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() ) } ) @@ -75,7 +66,6 @@ class DefaultSessionWellknownRetrieverTest { rageshakeUrl = "a_rageshake_url", brandColor = "#FF0000", notificationSound = "a_notification_sound.flac", - identityProviderAppScheme = "an_app_scheme", ) ) ) @@ -90,8 +80,7 @@ class DefaultSessionWellknownRetrieverTest { "registration_helper_url": "a_registration_url", "enforce_element_pro": true, "rageshake_url": "a_rageshake_url", - // Note the trailing comma, and the comment! - "other": true, + "other": true }""".trimIndent().toByteArray() ) }, @@ -104,7 +93,6 @@ class DefaultSessionWellknownRetrieverTest { rageshakeUrl = "a_rageshake_url", brandColor = null, notificationSound = null, - identityProviderAppScheme = null, ) ) ) @@ -135,118 +123,13 @@ class DefaultSessionWellknownRetrieverTest { assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) } - @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", - ) - ) - ) - } - - @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", - ) - ) - ) - // Next call returns the updated value - runCurrent() - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - anElementWellKnown() - ) - ) - } - - private fun TestScope.createDefaultSessionWellknownRetriever( + private fun createDefaultSessionWellknownRetriever( getUrlLambda: (String) -> Result, - jsonProvider: JsonProvider = DefaultJsonProvider(), - cacheStore: CacheStore = InMemoryCacheStore(), - systemClock: SystemClock = FakeSystemClock(), ) = DefaultSessionWellknownRetriever( matrixClient = FakeMatrixClient( userIdServerNameLambda = { "user.domain.org" }, getUrlLambda = getUrlLambda, ), - json = jsonProvider, - cacheStore = cacheStore, - systemClock = systemClock, - sessionCoroutineScope = backgroundScope, + json = DefaultJsonProvider(), ) - - 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 ae7d1c629c..7457aafc98 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 @@ -16,12 +16,10 @@ fun anElementWellKnown( rageshakeUrl: String? = null, brandColor: String? = null, notificationSound: String? = null, - identityProviderAppScheme: String? = null, ) = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, enforceElementPro = enforceElementPro, rageshakeUrl = rageshakeUrl, brandColor = brandColor, notificationSound = notificationSound, - identityProviderAppScheme = identityProviderAppScheme, ) diff --git a/libraries/workmanager/api/build.gradle.kts b/libraries/workmanager/api/build.gradle.kts index 238dc57664..b53ed40394 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 c1874bfa74..878edb6fe2 100644 --- a/libraries/workmanager/impl/build.gradle.kts +++ b/libraries/workmanager/impl/build.gradle.kts @@ -23,7 +23,6 @@ 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 5e9a8694be..93e92b81ce 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -39,13 +39,13 @@ private const val versionYear = 26 * Month of the version on 2 digits. Value must be in [1,12]. * Do not update this value. it is updated by the release script. */ -private const val versionMonth = 5 +private const val versionMonth = 4 /** * Release number in the month. Value must be in [0,99]. * Do not update this value. it is updated by the release script. */ -private const val versionReleaseNumber = 2 +private const val versionReleaseNumber = 3 object Versions { /** diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 3e01ffc25f..ce5c324ff4 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -104,7 +104,6 @@ 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")) @@ -121,7 +120,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:troubleshoot:impl")) implementation(project(":libraries:fullscreenintent:impl")) implementation(project(":libraries:wellknown:impl")) - implementation(project(":libraries:oauth:impl")) + implementation(project(":libraries:oidc: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..27e44e31b9 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -43,7 +43,6 @@ val excludedKoverSubProjects = listOf( ":libraries:core", ":libraries:coroutines", ":libraries:di", - ":libraries:rustls-tls", ":tests:detekt-rules", ":tests:konsist", ":tests:testutils", diff --git a/plugins/src/main/kotlin/extension/locales.kt b/plugins/src/main/kotlin/extension/locales.kt index 2774b078b5..650fbc4d72 100644 --- a/plugins/src/main/kotlin/extension/locales.kt +++ b/plugins/src/main/kotlin/extension/locales.kt @@ -5,7 +5,6 @@ package extension val locales = setOf( "be", "bg", - "ca", "cs", "cy", "da", diff --git a/screenshots/de/appnav.root_RootView_Day_0_de.png b/screenshots/de/appnav.root_RootView_Day_0_de.png index 2d0d9fab03..7f70b90751 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:c3082552bf96131ddba157697bb254e22d6277537759c32dfabb56a2e99a9138 -size 27116 +oid sha256:aa11b2165af4a12cd518fb3f9a06b91d9ce87b468705efc7a962bf34a8278fac +size 26284 diff --git a/screenshots/de/appnav.root_RootView_Day_1_de.png b/screenshots/de/appnav.root_RootView_Day_1_de.png index 6d1c994b27..e1796d211b 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:f9231272736124f1e54a3422310d2dd5c7ad81e850548886062df61de36165c4 -size 30868 +oid sha256:35a9ac561c2546fd94121cb0c80e1f5be1fe147f425be4d087b647ae0f204afd +size 29986 diff --git a/screenshots/de/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_de.png b/screenshots/de/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_de.png deleted file mode 100644 index 08a29ff28c..0000000000 --- a/screenshots/de/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ae20cc867db5b7163b12ed8afd841a704673d6539ea543b573dd0f7c560988c7 -size 59775 diff --git a/screenshots/de/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_de.png b/screenshots/de/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_de.png new file mode 100644 index 0000000000..4bedce8ba9 --- /dev/null +++ b/screenshots/de/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:869749d64cb3836b99a132685a2411b56c3d93c6db1e2adddf669cf75ab90187 +size 68410 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 0996d18bf0..bfa7251296 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:bc9627a6288987e446206dc54abb8e1319ba6996674f0b588decd5418eeafc1b -size 18688 +oid sha256:b5988ca59d9fcfaccbf4bc2929d7c135f67be0852e739caeb566b0518fac7f75 +size 19021 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_de.png index c975946b3d..f2b02ca79e 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad7189cbb855280fd2e12a5dc43d0062918f76ddcc4b5cc7677748cf08a4afa9 -size 34478 +oid sha256:03d7fd58010b5eb1bdefef76648e08431728f51527815db9df58ee0010f871da +size 34304 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_de.png index adfa2c9666..cf7fd18f5d 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17acafce434f31f50b3232c2bebcd3bb828c70031cb892d7f5a78704f5d58661 -size 36455 +oid sha256:bce8b45b24c38b3a991dffd06d1b2f79da728820ab93f649138ae071883aff60 +size 36269 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_de.png index ae6d2b8bd1..38a62af758 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f063ef08eaf607de6d7d7361046ac96f8932d232b1f5cc48125cc2ac04d30531 -size 45836 +oid sha256:241dd64dd76b17b3a024622963c809f8788e0c5ef5f841b9be500942e4618076 +size 45660 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_de.png index 777af38fbf..5b6e58f735 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f30f5d0f8e410f3addc64cd5fa7fbed8505bcd562a75de06cbf3a1e7a40ceb0 -size 46745 +oid sha256:24ce85ea23acfd4d062785c013f5b1faa96d6ab75c1bab242bf03b6e6e330b78 +size 46572 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_de.png index 16cf139fa9..100e2139d3 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ce82d48e8b0b0c78a978277886a9916775cc12fc28edeb2807f63afb879c6a5 -size 48236 +oid sha256:e36b034ca57ca657072a089fcb7f7cccd2a5ec9897f80077bba29f85f2dcc96c +size 48069 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_de.png index ae6d2b8bd1..38a62af758 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f063ef08eaf607de6d7d7361046ac96f8932d232b1f5cc48125cc2ac04d30531 -size 45836 +oid sha256:241dd64dd76b17b3a024622963c809f8788e0c5ef5f841b9be500942e4618076 +size 45660 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_de.png index 0fb00157c3..2e87baac06 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c844dd51824db5a123a2b7e564a001b3609245259ec25c601a181eb69362098 -size 46863 +oid sha256:0b1b8c43a078f16dd5424960c63bc42524ce73d5198834b5291f9bd874ee7b0a +size 46711 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_de.png index 08a4e04bba..5f28babc08 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b6138083429a82d1de576009df367a52361b39e5ac094d0c4abc0bb47e89550 -size 41888 +oid sha256:d25872cb05d6d9b0cca7c579f67e531a059251ed033b3fa10a8969d161c9b050 +size 39859 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_de.png index 371a8aca55..81083ef1a9 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3b6ee300dc038fbea819d19286d10bdf023872abda1bbe6d81b4fef97a2d934 -size 41341 +oid sha256:d2659371297c436c5aea18011221c081ec94299ba6a5643f5fda0214372b3096 +size 41515 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_de.png index 62ffd0f03e..9ba16e11d1 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d550d37eb3a14779dd33171843339729cf3457f9a2a6e9d41d89b6c9f52f2f0f -size 35489 +oid sha256:1845ce37a3c2cc763b745caeb6cfbc094b6bd65a3c8234dc8173a13c5377e038 +size 35299 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_de.png index 9a45949a2f..7538d489f0 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68ab96769960de6cc00a06b2ca59cbf4ad20e9788c9fcf09b4ddb126104359f7 -size 37773 +oid sha256:abcd7267209f3ca0881cfbe3c9457f024d4368a1c8fc6bba8a00ab89e95ffac0 +size 37563 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_de.png index fe057074c4..0608a5a46e 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8583c1a707b3784dc2daf279628d86d813046743f77568aa1acb0361ce7cb4e -size 47505 +oid sha256:099f4b603ee07216e66a75d5d08b8f17084648526ac14e4e611c7c090260b8fc +size 47304 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_de.png index fc4b29fedf..62300fdbf1 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9016458c07e6c5e8fd962fe1ef08d0da82572c867304fb3ff4f694f83c40f36c -size 48497 +oid sha256:dc0b352e7c81e7e6389985c4c29d1a7074bd733e56789f3f675bbecd6ff17be7 +size 48289 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_de.png index 62222688ca..fc8a1452d4 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ba0af2f22cd932c626df190f2139f11906934b4d5de16cc1fde9d35488552ab -size 50028 +oid sha256:800381effd5dcdd3f41d28defd29cb21c99d254de86a91c1e7f60f476a4e393c +size 49863 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_de.png index fe057074c4..0608a5a46e 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8583c1a707b3784dc2daf279628d86d813046743f77568aa1acb0361ce7cb4e -size 47505 +oid sha256:099f4b603ee07216e66a75d5d08b8f17084648526ac14e4e611c7c090260b8fc +size 47304 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_de.png index 5a68c04ad0..b47aaf7358 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab9a5bc72358625c7623b3156365c299eaec576e96d11ecc3946e92511815509 -size 48578 +oid sha256:79db769c4c524aa2831ddf2e714102c0f1a5de1a7855025e67598c8f261e65da +size 48388 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_de.png index 5771464dd2..84c90cefef 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:981e3075d5285a630932c02427cca3d46efad74a640f12d35da8e0f29fb4b779 -size 43474 +oid sha256:631556267b57adf739438e7a9c094c411273de3d74113216064268926499d292 +size 41188 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_de.png index 11f94e5f15..13ddf126ee 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d35a6a56027b453b845400ec2b5b5912eb5eff551b2c370a26110b584ed3d3a3 -size 42939 +oid sha256:d8c0999d16e140465ab18fff34650ac850cf650316f52b4a3dd9145e6f928d04 +size 43107 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 1cc92bddf4..21c6405a77 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:ac79f05488afb65d5675a8068ac1c70c7cddc81b1db5445f1314ce53a1da8ba1 -size 27143 +oid sha256:22e0fb8867a616d7d575f9262996f66730cb61f8b0e9c2c74fffbb92b3a1fd06 +size 25852 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 5edce6541b..062623eb08 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:9c1602858c58d4f05c5c3d816cde905af8cd888ae9337215119902b0dbdf1cbc -size 43609 +oid sha256:c629821f33feb647b508b7f6bea584f807bd0e46162784d1ac764f97b050e9c5 +size 36461 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 641671bf3f..afc83cfdd2 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:1acc89193300b0ea268c127c98c7d46b8e23841933750d99d597a8713e1698ed -size 36762 +oid sha256:003c99b1a807edd7a143bb5329d544a1519bfea1efbcd29c275d9b1bd0031a6f +size 29954 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 b9ce42a450..dae5be9d78 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:0f5bddbb812e5fd0863a8c1308349f627f0e8fd865ee36f995a29666e0816b0d -size 49843 +oid sha256:04a3913f1c1e74f6a78706dac9e1aa33e2520bd9c37857bf58a7e5a58f12ec17 +size 42559 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 f8be736047..2da29be9b1 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:5e141bd8ca4767ded168a1e7cb574df8d33797e05ced6615287469b282d985b1 -size 43319 +oid sha256:06df3ed8524a4ff0d1ea12c43b624dd4f977ac591632d6062dc116aab53111fd +size 36195 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 5697209279..61fb494655 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:15bba5407bbda9070cf2109165e4fb9455450c8d6dd504dea7b521640fcfa48d -size 33229 +oid sha256:e2e33198cfde05a0f48bded09ad60108993678ce6a84da6394cb0cca4df66edd +size 26295 diff --git a/screenshots/de/features.home.impl.components_RoomListContentView_Day_4_de.png b/screenshots/de/features.home.impl.components_RoomListContentView_Day_4_de.png index eecc5692c4..114c133d28 100644 --- a/screenshots/de/features.home.impl.components_RoomListContentView_Day_4_de.png +++ b/screenshots/de/features.home.impl.components_RoomListContentView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d80e64f2206d9d21b3467ea4a7d5b06a33391cd56b5d899f944e7bd501b1743 -size 64430 +oid sha256:2574ac92d1424f5224beab9ac060d731a763cdc95a4550f07fb22bd4bd393105 +size 55716 diff --git a/screenshots/de/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png b/screenshots/de/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png index 34f49e1141..5e1b3d5040 100644 --- a/screenshots/de/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png +++ b/screenshots/de/features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8bb68b889c934425bb53b0b923ddd86c5a6e9309dfb7d06505b986521ab75e7 -size 52580 +oid sha256:ea704578dfaea8ab3a4dd6eb42e45b40e4037819fe83586947136252a39855f2 +size 38819 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 deleted file mode 100644 index efd83f7af8..0000000000 --- a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f2e4ee3ec519388e7acdedac96636aa3f0e0d9dc4f4a061a0b3e4c345b824a3d -size 24961 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 deleted file mode 100644 index b7a581021f..0000000000 --- a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d367fc2d1c620eb370e28db890e4f01acd85b0273b241d0b0b701f23939fabc8 -size 25377 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 deleted file mode 100644 index 0c1e7996ff..0000000000 --- a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6175b377fcdb3f70739c36ceb1d59a7e108d8f1d08b4c10ec6d28744ff0f0545 -size 27442 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 new file mode 100644 index 0000000000..b51bbdbfc7 --- /dev/null +++ b/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_de.png @@ -0,0 +1,3 @@ +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 deleted file mode 100644 index 67b0361c5c..0000000000 --- a/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index c39d87d73c..0000000000 --- a/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 950a33e731..0000000000 --- a/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -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 new file mode 100644 index 0000000000..097191f4f2 --- /dev/null +++ b/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_de.png @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000000..d9912fb3e0 --- /dev/null +++ b/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_de.png @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000000..cc6b3c7a57 --- /dev/null +++ b/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7dada5eb605d5558baea8b01101bfbb8cc31338b2ce76035a8fcb449cdd8e77b +size 25776 diff --git a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png index e37cdbea74..e7b089068f 100644 --- a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png +++ b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d2644ac4e57a29d145c34f7752496e5985430760ef2f519f78588afcc2fbc70 -size 85745 +oid sha256:e46c26ec7af7c4d1c1bafc0fb8bda6ef4afe7dcc6ceb8a5a38f2872ccafbc6d0 +size 86687 diff --git a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png index 5bfa67b2fd..0d394678ce 100644 --- a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png +++ b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27e1b519904b26e0164f1bebd1dc1c28abe2f4352d107f07d66db7ed9dd59e52 -size 38165 +oid sha256:c50c374b59c957f4e007d9548c2e03ffb622fcba3de9bb0adb2e36336114263b +size 39053 diff --git a/screenshots/de/features.home.impl_HomeView_Day_13_de.png b/screenshots/de/features.home.impl_HomeView_Day_13_de.png index ae22fbde6b..1f5c8972bc 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_13_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba73b9e725eed082239a57c1233bb05e69a0a44dddaa64a4cc2b4a8e83130dfc -size 99560 +oid sha256:e3f9c61cbe5f80b7574765bfcc6a1bef06b434d35666ee4fa2b0ba6f09078352 +size 97503 diff --git a/screenshots/de/features.home.impl_HomeView_Day_4_de.png b/screenshots/de/features.home.impl_HomeView_Day_4_de.png index f5b92f61d9..40500e7b72 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_4_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c54f63ff4b9621dc8e34179f15033a4ff6d61dd7cb22b47e56b5e2b9fda113d4 -size 57802 +oid sha256:55944dde4104ac2de65a11ab041d813416a21aae25e4cc21b9f624a2d33a80c0 +size 58447 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 5fb0408a49..79a282bf32 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:d20416987d2485431b66ebb29af1f03efa2e04da7ef5875eda62d7c5e187052b -size 24828 +oid sha256:141bf23853b7ba97cffd4d54e9e0a48843d0a49e7f6d6a91c41f110d2df1c7dd +size 24180 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 4aa0388c8a..bc89b471f2 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:62d531d6b01266019e2fa686321e93d64bcffd5b1786f7ad4efac83fcc2a2fc5 -size 32141 +oid sha256:a39d0a1c752533bfb097ad1d2c59978123545ee76259e26efe6e3fbae209db69 +size 31441 diff --git a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_10_de.png b/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_10_de.png deleted file mode 100644 index 821bf59326..0000000000 --- a/screenshots/de/features.invitepeople.impl_InvitePeopleView_Day_10_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:569ac37ef5a1a15b294d70e0aba93dd2c4a8f8e2c23cc043e82759ab06ce14a6 -size 54965 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png index cab79d8a48..950394d6ef 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57454100c496cbf21eeb0dd858678a4e8c793d6d3ea8de908dd0878821046efa -size 41785 +oid sha256:b9b94629caba74d3fa8f9d939edc441b687c6496001b185c6b7dc4ba13a197d4 +size 41978 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 deleted file mode 100644 index 630aee04c5..0000000000 --- a/screenshots/de/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -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 308abe5a72..92ffd71139 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:a1e6cd137958c6f3cd9ddad7cdf9dcd35c9b9963cdab164c232ebefa50b7a81b -size 33815 +oid sha256:bf9aea8bd7c9f6d7eb7f31a71ec412ae0f143c60bc6834a1f2ad99e538d87875 +size 33758 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 7ea7736a31..0f07d0568c 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:06fa1d9cfcdd7e81dd5d08f584651d982136e7491041b4db6c0feb9875c2db49 -size 43509 +oid sha256:968c091419c902a4c135a4498ebe2422566d0f2dac72882e81ac5dc6572a9e77 +size 42859 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 0b411260ec..feacf82d89 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:0b41b171588b46f2b17fe366d311da43653c43dc1eccf539c7f02c12d1c7a21f -size 45688 +oid sha256:3e71f2781efd689a0c57613cf03fec734474128ae444b8079d6124f8b9aa19fb +size 45605 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 deleted file mode 100644 index cba542a991..0000000000 --- a/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_8_de.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index f79c47c87a..0000000000 --- a/screenshots/de/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -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 0e9606c453..1ea459175f 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:748da581859b8b1a05a2b5703c17b31f6725d26713053063dc825b425b4d4131 -size 19317 +oid sha256:91215fa6820bbe0d6da772de2720cde77bc731f28954d067c6a6a0d95709f624 +size 19238 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 7127f05f27..45ed4366dc 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:446b2dbace2a10eb7332886ce7fa83f992c5314f3a66fd402a13200e9f0f06b2 -size 25489 +oid sha256:419f4edd8b097ca40f68d5012c5b0c0e6c1301b6efcd01b13ce0c8e7a13996c6 +size 25430 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 c5645b1b44..a9af23935f 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:84da7365709446471e67dedd9f733786989f56fa1d3fe83ea5ec02344076983f -size 28966 +oid sha256:e40215a3f06166fa78319875ca074876e41e19a9155794f4d0b68a132045a276 +size 27975 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 f466ac0135..e172ea6f3d 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:a7b92adb8584cf31853a01bda52aacab97df832360399adbf63980ebb612ce76 -size 26029 +oid sha256:c1684330a9b215bb22fbefe2e59d456c9cbc31e75e01828c67a256b0a02b667f +size 25956 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 2fdab6284a..4b15ed217c 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:cde54cd972be78cb89278b4382d7b71297bdeddb0a16503211ed0342ad059a2b -size 26237 +oid sha256:6e96f99f81040aa3214a1acd937d25299d080ca0191258562cb08f78fbdeff7e +size 26188 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 fbda7d633d..75f85cba2e 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:76c3bacabeab51017a076a58ff61176ec45fff9711b86471cf1e6752692a201a -size 35056 +oid sha256:759da98dca2f26b5a7e87954fceba866e7fb526dc36587b62cb3d01f3ba29b53 +size 34992 diff --git a/screenshots/de/features.location.api.internal_StaticMapPlaceholder_Day_0_de.png b/screenshots/de/features.location.api.internal_StaticMapPlaceholder_Day_0_de.png index a9ddf2b614..f122f99f74 100644 --- a/screenshots/de/features.location.api.internal_StaticMapPlaceholder_Day_0_de.png +++ b/screenshots/de/features.location.api.internal_StaticMapPlaceholder_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ac712ad4762c520a1e10cd11f65e112ce0d09d6cdaffbe99ef00a03748991dc -size 295885 +oid sha256:212f8d1b300b7c1a4e4c3087018e81261b562fea9cf1fc1f2ef4a5443e3fc91d +size 440154 diff --git a/screenshots/de/features.location.api_LiveLocationSharingBanner_Day_0_de.png b/screenshots/de/features.location.api_LiveLocationSharingBanner_Day_0_de.png deleted file mode 100644 index f889a65156..0000000000 --- a/screenshots/de/features.location.api_LiveLocationSharingBanner_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:efd6cb8498b29f79769b3287a062ee1ddad3c34195aa5d653b8e28e4e6f973bc -size 10767 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 8381a894f4..f4290dda6e 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:6ad541415245d6e6ca95514db2aa053691d8d2c8193cf0268be67cc74b1af4ea -size 32928 +oid sha256:2eb237f3bea51645310fe28e66a374ee7eea722d262261faf7a2d4ad7bfc9515 +size 30400 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 bb93cfc575..7967b79a1c 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:1342bc5c63e6fa38797d1867b5006ebc91cfa926d189e4ff9060f84efd8146ca -size 26114 +oid sha256:4f148e3b2e061cc9cc1e3a6568f4452175b51341a2c317aed2c52a355351cdff +size 42513 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 deleted file mode 100644 index 0663eceb1d..0000000000 --- a/screenshots/de/features.location.impl.share_ShareLocationView_Day_7_de.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index bf8a621fe1..0000000000 --- a/screenshots/de/features.location.impl.share_ShareLocationView_Day_8_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a5606179cc7dacc1d0bc86ff0f38a7654cebeb7c428575e2da51d4af0ba12b9 -size 43384 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_0_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_0_de.png index d52980f113..d4d8b13367 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_0_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e8cb1edecba35913251d57d6eaa9d48149f7b2a26c180978c0ae34d3d354d97 -size 19693 +oid sha256:c27b436e8284ef32e2d4fe1285587f1580b4139b6e098d9e37e976c98f595057 +size 19318 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_1_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_1_de.png index 7793479410..61d38bcadf 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_1_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f66385d2afa0a3a4c68418c58b65bb9f073512842d6b601ac9ba7170a70f3d7 -size 20266 +oid sha256:c0e8730caeac7c344ae3d7c605aa944a6b8d021b5f3b8cfd387cf9cc2814c6c9 +size 40339 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_2_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_2_de.png index cf15f3c0ff..f3502aceac 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_2_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a93c8f459e118b5e97802055c076a31c260a8d1b9b863dec4464d79b5ece62db -size 16650 +oid sha256:4b70b5cc45b554c282a435f59d5311483569ad6be9875f25a1b88e1c9119b264 +size 36812 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_3_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_3_de.png index 03f56a96b1..d1aa86b7c6 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_3_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:534b3512dd38f27f313bbd8655667f3ecc104c5095e140f63b2e124277886d07 -size 40599 +oid sha256:016809a14091abf8554648ea4cd45945541395772c540d8aa8b04a24c0190050 +size 32120 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_4_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_4_de.png index 3b8f513f0b..d4d8b13367 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_4_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af69b59202b1e21069e13e444017d8e25ec467d8cbe5899448d74700dc1a765d -size 37091 +oid sha256:c27b436e8284ef32e2d4fe1285587f1580b4139b6e098d9e37e976c98f595057 +size 19318 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_5_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_5_de.png index 6a87af5d60..a0d42acd55 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_5_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8a5e9159c4817b5c500d721e378e51d1f3ed6bea5468b54e4e66db1969c292 -size 34873 +oid sha256:10244806518a4f64985be17fa5814d1523ef8796398a02632d48bcdf2d9daf53 +size 19451 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png deleted file mode 100644 index d52980f113..0000000000 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e8cb1edecba35913251d57d6eaa9d48149f7b2a26c180978c0ae34d3d354d97 -size 19693 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png deleted file mode 100644 index 0c68797fef..0000000000 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6fe56f20ce5792f9dd60bf0ae08fa5c5c78fbb7a5f1a80eb2fe973716be69c00 -size 19831 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_de.png index 1378a04aa5..d9b37fcbb6 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ace865ac209e2dee5944ea526d1fd8337236ae81c43f90635dbd9b2db905780 -size 38535 +oid sha256:7e9040a7720257db29bcc71a9466bfe081c832878f88baf52bdbbed390505a1b +size 37170 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_de.png index 5294d06956..48c92bc715 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc388c605c1c09710b6e979aa2ec6110964c9ef3020b7e8c3e7bfaf839a48133 -size 35383 +oid sha256:1adfeeb7e91aae96124e3062f20c029df73cef9e68bee2149155e1aa0fb7dfa7 +size 33956 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_de.png index 36c7aca115..b33b0c2150 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3cf2430c703ec8c38d0fa11bd486f8aa960822246338ada3e0bc4fa2c846b7d -size 24074 +oid sha256:7d9f3c98a0e112c49b365f20fb5c4403b6c2073f62072c457f5e7df9e4c979c3 +size 23555 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 deleted file mode 100644 index 048facf45b..0000000000 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_de.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index af1fe3af55..0000000000 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b481b2bafe288542d25c301aa568fdbd9d63170b1c59b01fdacacc69d0395b3 -size 17352 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..13ba4da40c 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:059452488e08fcb9f35afe0db8805665417108ed0593561251f648b91eb7c3ff +size 43688 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..6297eded9b 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:7b3ed6ddd42ca136fdb7ad0f18d03e112594c0d9de93264bc2659e1b3a4e1e75 +size 40614 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..ded859b551 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:1b6828f3878d95a0357b399072acce35b073767fdbbfea90fe9eca73f60af802 +size 31600 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 deleted file mode 100644 index 28afdb624e..0000000000 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_8_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e6f6d2946d194ba97c64e6749b1f9d8bf4ce52e685d06631cf22b47e156791d1 -size 41526 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 deleted file mode 100644 index 9c540f1299..0000000000 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_9_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b74c5fd9cb084ca2c391ea65dea23ecc027d1206510841ada0d28d5f6674995 -size 32940 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 ec9de6ee4e..c6a8d72619 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:228c78854193cffdf1a0a22a80eb742b01dcef195192196f7b63c32f3d0666e8 -size 30823 +oid sha256:6c3b505df79be2829a102e9a0249fc23c5f5de1e7fa19570a1fd1e5ab7f9aaee +size 30819 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 ec9de6ee4e..c6a8d72619 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:228c78854193cffdf1a0a22a80eb742b01dcef195192196f7b63c32f3d0666e8 -size 30823 +oid sha256:6c3b505df79be2829a102e9a0249fc23c5f5de1e7fa19570a1fd1e5ab7f9aaee +size 30819 diff --git a/screenshots/de/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_de.png b/screenshots/de/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_de.png deleted file mode 100644 index 6c4b3efc05..0000000000 --- a/screenshots/de/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:54dd4604f24579447336a906c9cad8b7f0d27f2b30770a136709d2ea964a8ecb -size 78928 diff --git a/screenshots/de/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_de.png b/screenshots/de/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_de.png deleted file mode 100644 index 68e2995e17..0000000000 --- a/screenshots/de/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7209ac29981ac25c206497fee6acacdf6fb8cbb1d0592b57ad343aa2bc8e4bbe -size 75196 diff --git a/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_de.png b/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_de.png index 7cd0b44e91..fc2a26b90e 100644 --- a/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_de.png +++ b/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac1d614f172213fb336f22ae114bbadc0560824276ecc6fb97904eaee2b1f0c3 -size 38783 +oid sha256:88d4448dbf7fad8cf6ec5075babdf09b5f46401a259a6ef3f9b380a4a3de0240 +size 38712 diff --git a/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_de.png b/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_de.png index 3fdbf2fe1c..77a79c8e8f 100644 --- a/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_de.png +++ b/screenshots/de/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f96859710af78fb5578821e553eae9c1b5111c35535d6ea2920b59ff63b573eb -size 39638 +oid sha256:06d30166f29f5335460e6cbaabc81d7cdf38b7043b1767f51976386fe2e35347 +size 39568 diff --git a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_8_de.png b/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_8_de.png deleted file mode 100644 index 0157c6d961..0000000000 --- a/screenshots/de/features.login.impl.screens.onboarding_OnBoardingView_Day_8_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d508cdc9742a8a2ff545599b106c0a57c84987d11940a03d416a69ff957aea4a -size 315614 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 7848b3de7f..50663c9d70 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:7ead8637651c4d0e3c0f6e78c596c4781d00ecb493bdf54ac96670b55ff541c2 -size 38217 +oid sha256:8153b9a20bb2cda60b59abb617f527b165f54fee15235384b580de74b2ae31aa +size 37912 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 308abe5a72..92ffd71139 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:a1e6cd137958c6f3cd9ddad7cdf9dcd35c9b9963cdab164c232ebefa50b7a81b -size 33815 +oid sha256:bf9aea8bd7c9f6d7eb7f31a71ec412ae0f143c60bc6834a1f2ad99e538d87875 +size 33758 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 da94eb9d22..3e1a99aec0 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:3eeb6eb4ff66c0558d2abc960a8bffc757d6a8fd043f9c4eee4667176de38617 -size 43485 +oid sha256:de35cb70fa0076e5c1d0ab35cdae0b48cac90aa0c0223538276054e69768fd2a +size 42776 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 49aa77efd2..11011887f3 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:e1c0c74a437c19c02f04c6de9898fc8fcce98ebdd0161f97d9973d6bf091db58 -size 27056 +oid sha256:4a4508363f01ad58b30503ff92236349b619feaa5716524438a2f52f033d71e6 +size 26225 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 4119ae071b..e55f0530fd 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:b742d5311fc1961bc9a5a61658e77fdf1ab3f83b5fb93e0a4fedd7f50a3faacb -size 56999 +oid sha256:f27bdc5b6fc21700a4f8512230084f1131d9804eb7cd4366d61f90deaf55bb60 +size 56835 diff --git a/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_1_de.png b/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_1_de.png index f284b228db..cce900f9c8 100644 --- a/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_1_de.png +++ b/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a89135e74bc17ed75126152bc1430e46c066549b42022995e19eb2e834ec2702 -size 27232 +oid sha256:ea70291958f6daa69da86f4479eb3c1c5398a6dc23ee8eb935ee2bf29e938861 +size 19022 diff --git a/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_2_de.png b/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_2_de.png index 3d3ad6e858..7ff2a4608d 100644 --- a/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_2_de.png +++ b/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cd4d7561202538ac0cea1953f038c397e355bd478b5a2eb2d59a3670c135938 -size 10949 +oid sha256:5d8127471c3c6f14883bec221a6ce637ccdf733582caef4497689a59462299cf +size 9489 diff --git a/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_3_de.png b/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_3_de.png index 51d3b483b4..de80fa70ba 100644 --- a/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_3_de.png +++ b/screenshots/de/features.logout.impl.direct_DefaultDirectLogoutView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67e1b58ec1118afb4dc8985f22df74e103313db8239d4b3650cd9052a258645b -size 22445 +oid sha256:cabdd1c4ad7e6256c232e10950f0d7c23410af2a5e91cf729265da0af6af8672 +size 20656 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 c19097e89a..a6428f063d 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:392e090f14a98249b1cf3cf4acf180a817c41dbc8b4320ad5b8a2b4636ba0f14 -size 86883 +oid sha256:7f7c52ed254e4c25516714274000fe41f00820cd2df2b8232628ce43cf5e95c4 +size 90825 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 c84f36df47..55443656e3 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:2488a2d453b8a8cd086e531e1c49ac2d41186713b85c1aba3df73cf6c7a5f1e8 -size 86605 +oid sha256:ea581062b4d0e6bf40089463632c4ed6dc3e5d1fa81667148f50e39a7763d76d +size 90544 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 8dd9a8233d..d95fa85ab0 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:f0870cc34ef448fc914f7f4a688781bacaca59edad593509ee5cb333fdcf4acd -size 73713 +oid sha256:e49596da984aafededd2ffe2c850ca3bea516e56f55e4e72cd5cedfcf6b4c4ef +size 79081 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 50752acb56..64e7962bbb 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:844f24409611ccc30b514427c72cd057ed838a1b4ba058a4c6dd941858908e65 -size 66529 +oid sha256:09fb626761ab736d5ea66e3883e215d0a1ec1a89155036eeed1775e9cdaf5ff1 +size 69761 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 e20e0c63c8..ecb40a0d36 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:cf40d60799deb62643ca650deb1a5b5e18f5d26ebca7927064c51a0f3a8b771a -size 57890 +oid sha256:45f01d0a582bdf3ecbf34bc6c6c32f29d1cf83a182e12e3df1b80da6574ff836 +size 61047 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_0_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_0_de.png index 6483c00de2..8da7ace03d 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_0_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9d61733ee67c899170d1aa57fec8e9b7b24c3f3b629cbcd48e2a16199f2bab3 -size 16988 +oid sha256:0acf2aa6b64219b503f168f51c61d89781ade323346a70df5fc55937f8573d25 +size 11407 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png index e689072a9b..a2fc5098f0 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72d2376c919a9d0fabe5324ac08659d23968e35a28636b76ba06cb5a45b713c4 -size 33299 +oid sha256:398ba71b5da9adf840da5fede416b5faaa712809dffebcb4e878929ceb634256 +size 30936 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png index 91e1a4ebbe..70b7056d37 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60070d82ca0753ca6d98e6ed84715a3dce70c7ccd8cb317e7504035e15c7347b -size 38499 +oid sha256:6f4f342e83f6d444c17c7c24b9371820c0e08352f649aa0b13a431fa70d8d8d4 +size 36160 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 7354b5bf41..894daf596a 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:08fa6f2925434cc451adb7296041af54941b7b9e126887bd529692fa208060ae -size 69544 +oid sha256:2f731b09dcfd8f2eff8958f2a294c9f8f969301c290d118fcb9446a7414652b9 +size 47022 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_2_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_2_de.png index 2d49c3747a..a4346a5c07 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_2_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5691fc1410a02d9526f73ab0e619056761909d9b65284decc089f4477e0b2acc -size 34918 +oid sha256:467e3b44603d9f9e62af61c2247224aa9fcb27ed0074da1884b94e7666a93bec +size 32548 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 7354b5bf41..894daf596a 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:08fa6f2925434cc451adb7296041af54941b7b9e126887bd529692fa208060ae -size 69544 +oid sha256:2f731b09dcfd8f2eff8958f2a294c9f8f969301c290d118fcb9446a7414652b9 +size 47022 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_4_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_4_de.png index 57162dc8af..02b28888b1 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_4_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:390cb6ad3a2918bf8a52641d6084829d8dc9ee518058765e04fa863e6a955b0b -size 38436 +oid sha256:eae66522bc3d44a9b54e5dc8c6abb9b2f18eded3f1ddccbb6cc3a167c4b4e53c +size 25861 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_5_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_5_de.png index b9b5c516aa..2511e03e32 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_5_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e34bac390005bfaab9f3c173c693c05390725f1b59033515ca3558dd1b551566 -size 22457 +oid sha256:8ab576bdeb5aa0dd38c748de53c1d7cc406d2ac857506483f04cd93acd51ba3a +size 17087 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_6_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_6_de.png index d80e6d6b42..650d435f5c 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_6_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:366028bdd1b02cf2f83977e01b4c8b0344950b5943874a193238ca180773b534 -size 33522 +oid sha256:b7fea817934002a4e7343199a281c2ce08e3b299e23efed82e4f1f68ab94f977 +size 27206 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 ed1172e6ae..49d605924a 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:fadcc1a9018cca35bd08be7f7f4d670e55ec362c15c0ef89625988da6be95ca9 -size 47451 +oid sha256:9a2b1dc530bc95a778be29a331fe765b80b79ecd3690afd58e296aa27f398279 +size 40887 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 dd9ecc5fed..958d6e6583 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:652613c8509079b395d964c0290272a09b6a9bf4ca02c18d6edcae0b43e06d16 -size 61053 +oid sha256:92f34785ad838bbadaa40c4ab3a602fc2644673f7df4ce320227558b57042ec1 +size 41545 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 dd9ecc5fed..aa8d8cc60d 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:652613c8509079b395d964c0290272a09b6a9bf4ca02c18d6edcae0b43e06d16 -size 61053 +oid sha256:8d8075c75345069f27546f566c039bc4d5632dacdf220a63b38a64fe88507340 +size 39385 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..dff61b2c88 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:ec1b6c85754dd2f0ba4b67a86ab8688df6c6471a53e508198b1d3af630d179ca +size 400952 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..77d7c16046 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:e82d2a12ee66397f1c114ae4836a8e513db4f1d87c1fc4501bc85efbb1e63f83 +size 62736 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..dff61b2c88 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:ec1b6c85754dd2f0ba4b67a86ab8688df6c6471a53e508198b1d3af630d179ca +size 400952 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..0c7eeacfb6 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:65925352e2e25b708f04c1adbdf5ec70a515a980776887a35351a8cd94ec94a8 +size 62591 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..041a34fe20 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:9392073a8a6600cae209985e789fa081496a2c5cedb772f835a73b05c49570aa +size 67711 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..8313f647f2 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:eb34a1ace20c04dd99090ef68d17924f3a24a475d1ce5d376969d18df95d315c +size 71910 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..f9b27b26a9 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:a3c995ea0668250953302992ddf3e535247dc5e972fd737313b5d352349839e6 +size 407904 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..d2e6e0ccb9 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:957d2be646cba8c187cf6583e4598b72b6449ac745eceb88b8cbe647e71ae236 +size 88554 diff --git a/screenshots/de/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_de.png b/screenshots/de/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_de.png index 9b2462e1d3..59a1f9a9cc 100644 --- a/screenshots/de/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c40eb122d941ba96cdccf2c97659fce4f5599c88f7aff0ef3e5d084abd0fb241 -size 27341 +oid sha256:f66fe5b25e3de0a28bfdeb0d45de675407af34e2767b59a8bf75cfc225e9dd5a +size 25446 diff --git a/screenshots/de/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_de.png b/screenshots/de/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_de.png index 56e8ed31c9..b957f75c10 100644 --- a/screenshots/de/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_de.png +++ b/screenshots/de/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b3f371d99fff430a018e02a3d64ff0e88fa615db43a3c48a66975fb00145970 -size 29875 +oid sha256:1930f51ff9b9d39cd9ea9a0addfcea9c5114b4f9e2449a57040deac0576f0498 +size 29491 diff --git a/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_de.png b/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_de.png index 70f452b628..aca8462ace 100644 --- a/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_de.png +++ b/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f199bc84eb10342e677b22e60c1b74bd9c1fe0b9842c474961e601c23c9d67ad -size 66719 +oid sha256:b3da0fcc021270fe7cfde72e03f44f9c95428f18673aa86bbfb11a6acc0b344f +size 65207 diff --git a/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_de.png b/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_de.png index 80ad7b4e48..6d95e18da8 100644 --- a/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_de.png +++ b/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:743455d5509d5e81ee5eb4012a03c5ae8c2002750d57f5d379435e590adb2990 -size 67544 +oid sha256:92c919777810db1c4d31897b44b3b35cf70ba6f244061f1539eb5efd4f20ede3 +size 66922 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..0ef8f31754 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:38522e07ec0c1e856ed94599090255f7ab3b032ac8edd824553b21da3eb50192 +size 26139 diff --git a/screenshots/de/features.messages.impl.messagecomposer_MessageComposerView_Day_0_de.png b/screenshots/de/features.messages.impl.messagecomposer_MessageComposerView_Day_0_de.png index ea8e6ae90c..e7fbb21056 100644 --- a/screenshots/de/features.messages.impl.messagecomposer_MessageComposerView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.messagecomposer_MessageComposerView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e07d15bb95f7385596d7f454620a282277f8ca42e258728d6dbc17b5da84f59 -size 18045 +oid sha256:ab856d0a2de029924cccaf9f2285f3178315d83fb12e02f7f71d9c69fdddc5d1 +size 17989 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_de.png index cae13d102e..0fa4f15cf4 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b5196548e23ad3308c3517c2764dafe73b620115d1721cd4eaa1967e795a174 -size 14262 +oid sha256:73ba15459015c981b063966fecfabb28af7b2bbf25d16412439f0f35e6d6694c +size 12924 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 1c168e3cfe..af84cc0319 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:e78774044b40b63565ef540b6c1fae9fd97c105ffcc049e524e8a80d88435b50 -size 27225 +oid sha256:579709deae308329674cf4d47805485699f42c67e6b6779b78e95f45437aab4d +size 26045 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 deleted file mode 100644 index a854d161d7..0000000000 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ad01125850f8dc08f39832fe3ffcaae1950edd736a80f39811b4c1d9a6eafd93 -size 122264 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 deleted file mode 100644 index a8d7c0385c..0000000000 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 2ed9893cb4..0000000000 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dbe9d2831b54807db3c329ac4ca9e853198195df21f8db348050b40fe98cab5f -size 121228 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_de.png deleted file mode 100644 index 9fc7fac5f8..0000000000 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:62abb24d710f2bbcd5965887318ac1c1ed7af2aa7ead64fa8557781e3c03ba9d -size 21189 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 b3f6706cda..67fec85a88 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:29fb6c5ccbc3fc1196fbccc4ce18d10668ae47733ecfcc454354ff231cac0f10 -size 56030 +oid sha256:68d468cf24f004f75b5f0d0ab21e4576941749b32a6780b074fce5ba6e7e06c0 +size 56172 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 3ece744fcb..72c99d1da9 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:cbc7e8874d0a3c25aa0e6036c1ffc365c5407d79b011e5321821691315e98d3a -size 6223 +oid sha256:b10f43a24d80689ee7c929eb5c530b544891cb0dce63ab9bbb4618f999fbd3cd +size 6226 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 d005896ab7..c9a5dda40a 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:08984e24e297742f065201c4cea3d07eda34e4c80fbcb4689e539ac64f32f1db -size 53795 +oid sha256:60f89baf13d98c9060d9346883cc72b23308442604eb589bbfe1d6ba5e180db5 +size 57070 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_de.png index 7a936dbc60..ac8ba29d96 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5646e1e203737e229787944dd524377f51fe1478507c9a6dd19916716526404 -size 38381 +oid sha256:fe8e088ca91657f0d834b77169905afb29f610c8abc04ccedad4ce408216c032 +size 37992 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png deleted file mode 100644 index abb112a538..0000000000 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4c119e44d9f51d0b4786f06092d3d0138ef69b363c43303164b0aac0629b324e -size 69840 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 2190897314..6d76726db9 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:f952ad332aea788e87445b304894f0acf77a6ca5498d67584ab4e727cbb16eb4 -size 58055 +oid sha256:d61417b446f42bf388970c65e8df1f8ceaf87d457c55cc1cdbdc60d2ebe0b5a5 +size 54834 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_0_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_0_de.png index 9dcacf8924..b355228561 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_0_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb6ca0ade3d54ffc371ea734a0aba16d27b192b8a602e79355cdc45d1a3fb33b -size 56639 +oid sha256:da02621bac48d9f9d7349dfc02244dcbc698442f1a8975d2c9b9d8e2d9c64fab +size 56650 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 63497b4081..6a6d9271cc 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_10_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7cf06d5921dae7bbdd3b774e38ac6fcbad724261c1adef9ad93ff992fcc7669 -size 51515 +oid sha256:d5c26f5383e1d079f5fa8a62baff668032181568a52bbbbc90bc2ee5d70d78a7 +size 67208 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png deleted file mode 100644 index 98dd645229..0000000000 --- a/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png +++ /dev/null @@ -1,3 +0,0 @@ -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..80bd625605 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:26faad6c5e6bb011b3df1eb723558c22383eab2b2ac53e906349807afe1ac7a5 +size 41131 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_3_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_3_de.png index 9cfff702c3..47d2038cb4 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_3_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d4c8687f4067cb9de6cdecffe70d9a91471ab6298a601b64336211459fd03da -size 55948 +oid sha256:3b7c1a47a6a122af432404385fe938f3da344c1c1aeeca3a6d4faae95c377470 +size 55932 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_4_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_4_de.png index 7cbc28b7a1..e451952b98 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_4_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b254226cc6b00d4871855a5c566d2f35cbbabe2048a58f12e86cd07372a2d91 -size 54071 +oid sha256:0db0d106c3ebdafc7242ae1f0ebbe9211b035fa6d75b6b092a926052420aa920 +size 54067 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_5_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_5_de.png index 88897e9150..2997d59585 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_5_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10845895f042e2fabfdf9ab573ceaac779ee30ecede1c76e5bc68d78b51bf985 -size 60476 +oid sha256:ba00ca1bb5a5abe1f58944b3d73d85d78b797126aa76873efa03bfff3df0b8ca +size 60458 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_7_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_7_de.png index 7c18c364e1..fcb59350b1 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_7_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bf1fd981150ed276bafe5b2e4f710c47ad423c085267ba65070b1a2806ca078 -size 59937 +oid sha256:a0ba9fbfdf7e5c21c9461a103c5e552d8585ccfc152507ca701c5a8ff28fee49 +size 59958 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 194a69f62b..b713fac540 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:cd28a4f1e30c5ef7db0cd167a3d0328af7b969ccd7c35f616472dc2ab02b5c4a -size 57867 +oid sha256:e08f56a74a85de1d5e792dbc17a409f95c0a72df49b3c2538814442fdd63d4a9 +size 65018 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 b713fac540..76cb8f4e26 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:e08f56a74a85de1d5e792dbc17a409f95c0a72df49b3c2538814442fdd63d4a9 -size 65018 +oid sha256:3f263db2048cf39ed38c4895b5f76e6bc9c9d0fb8de09a745a324c47e695b1c1 +size 51533 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_de.png deleted file mode 100644 index 0793b01c92..0000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index c50a3e35cf..0000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index eb4c703d89..0000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 4e5adc17e1..0000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 33a8e007e4..0000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 61e1649e93..0000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index f48a864d6d..0000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 3c66dd1128..0000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 1eca9c9326..0000000000 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 246e46b33d..0bf95ba4b1 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:a060a4ef3ae0e7117863606fac9196536fce38d5483e3d882e86931d5f6098e1 -size 57951 +oid sha256:030671a61e231a9c87a221e20e63bac8add5f0a3fe4aa967313af45196151c64 +size 51196 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 248175a402..8ece7cdeaa 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:9e4faac64c7b38a8b9613eddc62805a5816ed340984e62dc5d6625a79782d3e6 -size 57811 +oid sha256:c813fcb2b9cf76749d9233d5cb66e643e185ac3248eab4c4fdd71f4c7fab7ba4 +size 51057 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 c8bc853a8e..89c8b5a536 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:7e4713a87444130061d605edcf65d67d5aa1d6730e8afee1ba41c751c2dac77f -size 57804 +oid sha256:66feb2ac420379ece0b85ffe53d5a25a8d11cbe27133313dd9e679d582c384fc +size 51052 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 19d124ead1..cff6d2b2f4 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:d8907dbe4553c7aac67cd5007dffc12a06e3c3f218fa95a75c390ee8982cf117 -size 57815 +oid sha256:3fd601997ff6e5af28e70c25c7c8c2a61c37e47e736b955b1510ac67e3a05034 +size 51064 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 26bdb4b258..fba8ff45f9 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:961a4dd2ef4c74770f9c140f4b337146225d7a626ca2082f35cc107c91263e12 -size 57670 +oid sha256:6b089d766841cc1ce24ea03d3d91b59d0f2ea1789aee3a9d06ece551c862d1ce +size 50901 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 dca7c97656..fb27f001cd 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:0ea1eb184dcfcf13c8673f3df717a540c74eb500b759cd752e8fd270da9b8f4e -size 57951 +oid sha256:2b38f2502dd31806ffab41547b66b9987de66c4e899b0df59c068d08ed4350b6 +size 51198 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 53eb657d5f..0429803048 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:ad17f959304774a8956cc38cb008024e79ae70dd1d2f147b2de20a95ca29ca96 -size 57638 +oid sha256:2ce5af26d03b80ad7bb0190b6b1dcdbdba4f7a666c442109da58f05c07505aad +size 50867 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 1d51b69a1a..27f6862b8c 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:85693d212e3726d4e7bc5cd3c4c2e9d73276cac7a6cba7b9549d5b45bf55b686 -size 57160 +oid sha256:536cbfdce1cfdecdbf076f0747c50f2db7e3a88dda54c0708d4b7388c9efbf30 +size 50402 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 5736128377..0d41a18697 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:aca6e0c7f9d7c48ca80c5a8e56a86e8d0a1cced6020d5b4ef0aae443575071ae -size 60239 +oid sha256:20285a2d5b10f74f927f8459a2d92180b7f86bc266c95889b5d017679b622462 +size 58368 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 36df3fb547..ab828c858a 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:7713d5bb6252597bfc465fc85b3283e08d95684f9d0a17fc89e08f5cf8e870e4 -size 60327 +oid sha256:5ff37f653ce34795f7d054e1d552c161c83fe85b6c80c6925953440a75fd4ea4 +size 53181 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 833794c51e..6583fc352d 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:59b93100a819cc8a0b8b8156da39f47034745bfc6d06957907009ff3eef3c2b0 -size 60210 +oid sha256:4d69ff7dc1c3efb334b8a03dbd796cde640aecac07a10adb910519810bddefed +size 53065 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 25914b7cbe..1080a3e297 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:eaaf5ae88296f64e80ebbe60a126a1fd40944e31311329cade209631f36424cd -size 60223 +oid sha256:827f3291c252fa8a5b938ca7a38a76d0d58e1969607f2c5514d06264e10a1ddd +size 53078 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 2af3d0fc1b..1b77b7d963 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:77c71a0c84d3fa9e39ba3c9b9b81e8a3b6e406af87e4158bfb8bb4736a8b27c7 -size 60217 +oid sha256:bb8636f0a7dbad6b32346cad952b5447a79b96d3db0e5da876fca7a78ced91c2 +size 53072 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 9f0b5c037c..3048b141f6 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:c9228e4ace7d30444a45deb04f211782ca53c5a366e0e812145b5b772fa51d3b -size 60085 +oid sha256:24dfd7e781f8b035045ca075b607676a2a4cbc0ef882c1f527252d85339a738e +size 52922 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 a880ff90f4..8ebc7b90cd 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:c00205667f22842c1c5d63c20c0990b6e0fb7f7cd134f625957df733627378df -size 60330 +oid sha256:640ecec1abd4992b962a9ab8ce68d4c5a3accfde9116b93af4d55d8365c35307 +size 53187 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 4265684997..c1dda2d9d5 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:cbed351c3ce6dfcb334d28316501baa5f97ade4614bd9b7dbbd91b0ce7689994 -size 59987 +oid sha256:dc3da8b4380809f5f9724d98ba4eceb86ea9d6a7df54c4a2117d187550b46597 +size 52800 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 dd801a4745..419eec6b72 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:09bcae54d99e6b9279794ebebf32a67f67a8347c94f3286243fe8d2c0a92f352 -size 59623 +oid sha256:c03b9b002d3d5a42c6a5662301e8feda20f734a299dbd753b4fa797023ad697f +size 52372 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 2e74f12057..75634a5633 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:5d0ccc252fedce22f568f8d26f25a8600c21236e1d489a4dd59ccd63c1085227 -size 62771 +oid sha256:7abc46138c98086dab38fc368d56e06ea6a434bfe61b0023a665b40e66f0aa53 +size 60801 diff --git a/screenshots/de/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_de.png b/screenshots/de/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_de.png deleted file mode 100644 index 7fe20b1ba2..0000000000 --- a/screenshots/de/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:acd9db5acab1221cd8e78ba1fa3679da5e84c6c3e648e87c7143753bb636e498 -size 59053 diff --git a/screenshots/de/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_de.png b/screenshots/de/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_de.png deleted file mode 100644 index 74819b40b1..0000000000 --- a/screenshots/de/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4c3a9be1c0c98f2735fd882bf6ccee1efb4b2ba3f14ef161b8b4401cb01ad19d -size 56242 diff --git a/screenshots/de/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_de.png b/screenshots/de/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_de.png deleted file mode 100644 index 61c5d7bad5..0000000000 --- a/screenshots/de/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cfd699dea1779a7a04f8c27b7e1034ed366f21b4248791786ebd521cf942f38f -size 53865 diff --git a/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_0_de.png b/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_0_de.png index c90222ce97..ee2c10a62c 100644 --- a/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_0_de.png +++ b/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99f92671020a73a3350a84e30fbe8915e0d8704f846a15bc01a56ac17b223e87 -size 56493 +oid sha256:cf136b30186c020c761496b14baa482a01a786eacd1e335e59bfe991fbe4d6b2 +size 49391 diff --git a/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_1_de.png b/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_1_de.png index 20639717fc..fc82156d3e 100644 --- a/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_1_de.png +++ b/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ab82b153feaaa1b8f43f9a3586387e87fd598b9055ebb6d3a6ecb81c7f071f2 -size 47418 +oid sha256:0c2553146562f2e8a6ee5144cb06d482a4928fd3e155aef90ca25cda9a99267d +size 44282 diff --git a/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_2_de.png b/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_2_de.png index c90222ce97..063b8ba357 100644 --- a/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_2_de.png +++ b/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99f92671020a73a3350a84e30fbe8915e0d8704f846a15bc01a56ac17b223e87 -size 56493 +oid sha256:30c484fe8b370b96c3515bc1232ebb24495763b7b47c53d08d24fcebac62770e +size 46996 diff --git a/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_3_de.png b/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_3_de.png new file mode 100644 index 0000000000..ee2c10a62c --- /dev/null +++ b/screenshots/de/features.preferences.impl.developer_DeveloperSettingsView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf136b30186c020c761496b14baa482a01a786eacd1e335e59bfe991fbe4d6b2 +size 49391 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 9827329bbd..ecad978eea 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:ece0f3e49ca489a67aa7a489bc2d7e4f35a99cff4e4f2df3f7ab480965b3f972 -size 44150 +oid sha256:4d57b5854a0156811f11b23a51f347387183507d95da7b68a94f9ac211ec7f36 +size 42003 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 28e4e403fe..78e516e447 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:2b8741879622a799c9cae5ba7ecb0f83700cdfca52dd9e6b281bfa0cc31267b3 -size 26817 +oid sha256:8ef5f886b293e35592e5c41b8e84a21995982ed56ade44f0e4dadd83767202df +size 41800 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png deleted file mode 100644 index 2f7d54a7ac..0000000000 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 61aba383d1..0000000000 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 672f329d0d..0000000000 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 5d238d5c65..0000000000 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_5_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 ba560e522f..247fa79f85 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:7fff1e6cc2fe7ae2fe3dc81ee1f13f096245c238f3ca946ba4a353e693d0ecb8 -size 45468 +oid sha256:cc5c25affd9692e0c48c8160a444d98771ba5d725554024ffa0bcdbf00e49b9c +size 43020 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 893d7a086f..2346a013bb 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:e335a7132330e2c5dfb3f4cc015e25fdf67af2030474b347df2e0cca343b1c74 -size 27763 +oid sha256:7edf11d971f30998917d1ff9b3fcce43aef543a96289eea074be8d3a9eb37fab +size 43077 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png deleted file mode 100644 index 7a2abd7f42..0000000000 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 3b526c2fc1..0000000000 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 57bed01c00..0000000000 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 4537f0ff7f..0000000000 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_5_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f7a69722f7e5f6373a266116e7d97bef5e620b136deb2cea30409c41ec8d4180 -size 21310 diff --git a/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_de.png b/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_de.png deleted file mode 100644 index 717c042d39..0000000000 --- a/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d14d6b601dc1b0e75cfb8a981d50be84038a544a83837cc5ea5ce2b415ff6226 -size 20879 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 6fe8d49af7..29240c9037 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:225e3ea78d7f1bf3bb9c2c56c128bdb6994b22df3f7c010837ef37bbcdc34997 -size 25170 +oid sha256:5d34090b6d2a05946f81c205b40dba420f49c19063b8f4bf2d0f55afe31886e7 +size 24441 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 c78691eca9..39aa8e9965 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:7546c33148ccc5d46e112c2488c6aecce925a9a0d729cc9c029c9dac2578e2a7 -size 28753 +oid sha256:b2e6807ba4b85c71cca8aebaf34a22c836c6d53ebf8caaa42790594d3066956c +size 27850 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png index 9dfcbcf1f6..e38435bab5 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce3c8e771e43c186cadcf495a7d85d6ad36c089373782c209170282e0e5fb1fb -size 49709 +oid sha256:17ad93c86555868ac098ce3beadb8dbde57c89af9bc2caec61df1f3596325ae4 +size 46178 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png index 26411218a7..23fc5ecd1f 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db7a53d858a08f1396a9c04b6a253623fa25160f90de62be9dc187f72fccedbd -size 48374 +oid sha256:d16ff2356729e9d5aea99f00b110efff09d6c0beaa33e100846f3c56cb0fb0a9 +size 44757 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png index db935d5dc9..fa145bb3cd 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38e49c07c2f49bee8ba4d37f684be34ccb247bfecbfea5b1733e76246d160a45 -size 47086 +oid sha256:e8c48700213f1ac24b20c08d8585bea693ae08cf4a3dc91e6ac6aa08c1a02cb4 +size 43518 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png index d0343f2f12..564d0ae6bc 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f91bfd31c40b1cea5b4dd763dbc92eaaf7335d078ac496b51b05617c01f8b96 -size 48934 +oid sha256:6f97df43c4f6e479e7ccdaa94d0e817f2ab3f02cf21986df1af09805eed8bd59 +size 45340 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png index 12ccf2ffc3..90d69eb98c 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d85a9aa1456dccaef8a7cb2c8b8c756e54c4fe03402d39d1240624d279449c28 -size 48821 +oid sha256:75b4afed84d26d914eb82cdef4d9b7757d7cb27b0ee938c2bcca48d10553ced9 +size 45253 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png index 625ef06b29..85c3b70cf2 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b65c937240f2a5bee4e4170a511fff5437c5bce47fb987674548487fcf72fd1 -size 49350 +oid sha256:b7d85799138c9367a1229f36d90481577f08e027c4e946789f48e098b3927a0e +size 45832 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png index 0ab84d817e..133d94e153 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f965ec5c45372e04fdcdc1c3c65633551f83e8dede75df8e8c329f849f487bf -size 49885 +oid sha256:3ffbe5ab44b8cd51b94d766ff8bf0cfe05b56152d94a75d2858aca6b25db0bb8 +size 46364 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png index 4798c4bc07..95a9128cc5 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b68d23accbe81e6913d16ef6bb15712105422911905f274d66ef656f98d8a28f -size 49201 +oid sha256:18f4ef21e7a652f29dc88206e514ad600c05cc9dd2a6a3532e4aa3e68894d7ba +size 45621 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png index 11a68a7982..eee91513e2 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3699ee438dbbd4aa0e5c5cd5a054304888421cb8c8a822dc52cb8369816f7feb -size 48429 +oid sha256:d04a9c799ef61bf131131e527c8734dd8ca67880d92a9ad121b576bc11d4a992 +size 44835 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png index de0323860c..d99db83de0 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3eada7d271b1f649d931e5077fc04e232046fd4171881c673bab8eaf990f92b -size 44452 +oid sha256:cd41d6b3943d4c5e74090acb90a712b930be74c49c6a39fbe2f3b60e90855fd0 +size 42886 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png index 65d67f1746..d02979f393 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:934be11ba08bc83461c1d859b3cf8b6f367feb89f6a779689ece209c30d8eaa8 -size 44441 +oid sha256:17e33f973d9e33f6e06e7b2b66d448d66183a353ed76c1d7712a13e2af9cbbc4 +size 42839 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png index 637fe31e2f..e0520466b7 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:540a461ec0a84d22269abc644dd62444223818e337edbab2f79317f80cc24696 -size 44107 +oid sha256:ccc14ef7b66c52afcb6d806b4d8d170ffb83b7817a5ecfaffd706853bbc35cf3 +size 41743 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_20_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_20_de.png index bfa129700d..d8814411f0 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_20_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_20_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30918ebc116ebf2058aa619debfa9165d5ef95feb916a83e94e49d09377435e8 -size 49193 +oid sha256:33521284c24815367a302bde9c49b2795ecb5d803ac322220f720c5269605ea8 +size 47818 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_21_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_21_de.png index 8a5276b209..e89ffa97bd 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_21_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_21_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cf3437703035eec0dd9623032bb62f1472b1a5fe996b7b85b9050385eb118ba -size 49109 +oid sha256:4f16ad28caae15b4bd13fa61dd5b95450a559b00dc005b157fecf44c9b7df625 +size 47553 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_22_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_22_de.png index a9e5ff3132..3ab82edebb 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_22_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_22_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:983d019c59d97d4bbe5b42562d6d028bb094a5b405fb2adc033232838952890d -size 48673 +oid sha256:5c493224b3437d144e064f91a9dcc97779953cddd81aa5884631ca0046d72dad +size 47264 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png index 404ba5c5c1..15c3785853 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c79eb29314fbb2378034b03d368d5ca49ae49c6a53387784445a0b97e3cc56e -size 43324 +oid sha256:6ab93cdb57a580a52ed48dde779355440d6a67412f9699927a8d4214388fc4a0 +size 39451 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png index b492032c83..ac8ea4aa9b 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d605c8379ff6844f7581231574cdfb50e5bce0b2e33c886fd43b520576f9149a -size 45963 +oid sha256:2d1c6f0c3898e000774de48cd7590c44bc11c297163735f16cc2dccaaf23e538 +size 45428 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png index 09c0f584a7..85b757fb86 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:355905f6a0fc25abe31db1359f24af1acb60bb3a013e697663c3ee9f87a9be52 -size 47386 +oid sha256:647eaeb0c9ddce71bf99e382fa45bfc560affe38e8581e12016a1e5a37e270ba +size 44587 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png index 0fd7d3e667..ddac91c4b5 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a378c9b3a9bde80050444029ec6eaaaf76a5182c45babc898dbea24207466a32 -size 44054 +oid sha256:2b129c0c7e62d3874d1b7e466dbbca271873b20d3155981eafdbd9c33202eb04 +size 42527 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png index 6392ad39ea..a4d831a49c 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:737de18454bbb78bd9ca789ba1ab821f203c54d55301a5851ae01e336cc42260 -size 45324 +oid sha256:35aab3535b161bb3b111a9a352f510b43a25f9c98a7e5648aa3a380cd8402063 +size 45969 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png index 425e08b224..a85951f6d6 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2ce5bb8422a493541b30fe3eb95f756367358e039b759967e1f4d96f7cf337a -size 49729 +oid sha256:eb393ca334e520249176e392cbf9f495c8651904f2e03680bd740f4d9b1c1ade +size 46076 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png index 6be8a7cc91..d8ac60f111 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4fd1c7495908336646a05655f1c4c2d2ea0ece1e690f69164cc2946273a077c -size 49038 +oid sha256:6d420ac1d49b4f2a47d00e6fc273e2f2768b14a05f0d10c8c655cb492abfe142 +size 45474 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png index 8927152505..3ec8b090c2 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12d25ae598f1a05aa86f0125b6acbaa4137becc2900a2ac649880c521ba2ef52 -size 48034 +oid sha256:8435264c98cefccc80e7a4bd32d903708e878f4c4ad2686aa35df8fdb9324882 +size 44159 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png index 86a072cb2b..8d470aa1eb 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7382605cb34650919a0b9f1d1e9460e28ce963dc4ff899fc6906dfcdd73dda50 -size 50783 +oid sha256:c0cdc554f6e3ce1725e2459ca0c6133f7f14a15d730f190bbbb795eb11c0404e +size 47224 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png index b3d5dd204c..dc8f2611d0 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6f057a687620b49fde08e0c241aadca011d87154e73b7cbaf21a22896de07c9 -size 49348 +oid sha256:d408175ca51c0b36ec01cd950b079467e29bdfdec9b18a1b81f1480ffff51e6b +size 45740 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png index abf7a28899..5e1712b9a9 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a08b46085a213f7c6a93f24c2afc04cab4a0c30369d6eaf552cc6e58f50aea34 -size 48160 +oid sha256:3bc00b126e3820666bad9a4904b3956ab391a6af0f74ce30794373af1c654ea8 +size 44592 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png index 257169012b..6f725c591f 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e624adcc59354fc21c960547d0766929b95e1a2745b667eb57dd5e62d3ca8451 -size 49914 +oid sha256:438edecdc96de067dfef71395ca0dbde45b5127f5a8d507dd4148f43da12f1c2 +size 46370 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png index 3804c84919..db3edbd7ed 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60cc99a113658fc4222b68b4801066c112d02dbd5c5b39e12471f77bb71ab348 -size 49807 +oid sha256:2272a4633116a8f58a24273fe348e4cf979eb31fa78a641ea6da28345e358846 +size 46265 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png index 080e2d2199..8d78460895 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd16c5e9846823589ad5b22568c25b0cf3335cba8b467ceb3c9186e5b84e7fc7 -size 50318 +oid sha256:a45ea374ed7827a1aaddcb6b5d0eb344b4c0d79ae328099bdff79a8a3ef84b5d +size 46769 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png index f80834a2e0..f7d40446e8 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d3dbdcfe1dedaabb5d03dbdcc2a2f377fb6d1aafccd6b70cb147a97bf23b1c5 -size 50929 +oid sha256:d4ba031a35b259142083b6fd508b7ab15cf169f7f5b1535bd81ef7e4fbdc0a6d +size 47370 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png index b43767563e..fe75d48f6a 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0761670005dd4463b662efb563a5270f89790470017d42001d00ce44e96f045c -size 50201 +oid sha256:7e87581df1ada41c9b7b85f576da8c32f856203a6c48e7f40128fbf30c7a33a0 +size 46646 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png index 3c0a33e5fc..56080a7551 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6361321527d1bfa1ea84b361dec64c3746a85079a4048666e4016f52d3d34246 -size 49681 +oid sha256:d87b127824291e0343f53cf50a22f0e1e8b89e00be55ea5f625aac59f9109a4d +size 46142 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png index c3b0b28c5d..e9d5014602 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5bbd36791567c89996dcd755b3ff4f956f69f1ff3f6f66595f16e1e733fe0a7 -size 45751 +oid sha256:827747ea2eae177d460d615b9da37db515f635ed433567945e1d88cb39337594 +size 43944 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png index 7dc11fb06d..8bf44cdefc 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bf0969b8a24c8d06fb3253646d6e2d0a94572cc346a8a9f9522ae48e5b4217a -size 45686 +oid sha256:1765877cdc96b174cb7a2e087165c74d1ceca02137d0daa510ec247cc4f481b6 +size 43823 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png index 4fb1d5bebc..52c52e8769 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cfbd204c871feea36d0ac5ec740049488a9d3511156cf7a3e30d724d62fbdae -size 45187 +oid sha256:6f3c5533e239f54302eb7867bda7571fd966b4c298be9ec48abad6a5025c22d7 +size 42890 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_20_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_20_de.png index 96b2a06800..c510c5f777 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_20_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_20_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84af752733042d4d487a934144525bbcf31cf9c1cb1454a5561ea9b3bf794afd -size 50205 +oid sha256:abb0b25a08a6507a9b3c0f3ed79ca11b26dcc54a9350a0711f9385ccb0ffa29a +size 48855 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_21_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_21_de.png index 3576b6b30c..aadf561762 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_21_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_21_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a5c142c8539e0a7d40f4455e96d3d5fe1003f2cef84095709f15fd63b945a06 -size 50130 +oid sha256:f615089fb33bf68a1f24bc1d7f78ba899ea80c5dd5e046b05df04055e7d15ed3 +size 48571 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_22_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_22_de.png index 6905512be4..2c22f9b92c 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_22_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_22_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:375274f827cb0b1a56461a95ebbbc12ab8dead23a41059b0909d87892b5b7f74 -size 49645 +oid sha256:c9f1b2f0372b538dfc219ac8ac699347a199f0df903813ad1a97b7e158f50f9f +size 48241 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png index 23620c662b..22634ff9dc 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9737debc2f342c5ce2b20f48151597e164526297d2789b04bfda74e4cc70153c -size 44357 +oid sha256:ab5386c966f4b5981bee9d39cd6a315bdc86dd286ff2c5e08aca4c5bb3304b23 +size 40457 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png index aca9ef2193..95c8bffb80 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d16e1a72ac9f47e0236b5ce3bf82c7e8d0d3ca0b245aba5fc751a0b9bed6cae -size 46946 +oid sha256:39e4eab91f3d07daff1437991ec74005e7c79eed3f9b4ff71c154d68bbce3e71 +size 46400 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png index ab1ea77140..7574b9b61d 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e483482b88e7edc6724459da394f87a587ada9ddefea5d05d9bdcfe57e34ec47 -size 48423 +oid sha256:49009a3a070a668b7487ce69b3bf1377ce113cf003e1571db3f90d8319ec084c +size 45597 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png index bd270b4ed0..00e31d8cd9 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c8130c023137a1c3872202df289f364a3cc85217c7b0d7d36609d1fe896e96c -size 45304 +oid sha256:9e268c85951e089414695eed071ec5c98a80030d130f92d8a934f815dcc75334 +size 43480 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png index 26e23487c0..71787b6f1f 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c67de39146bb8c0049de90223d4a7293e3f42ab8f3780cc0980e62c97d10760 -size 46651 +oid sha256:eb164e2c9796a1d12a0146b3fb67d29813be54d42b0628d17a323a525b866177 +size 47065 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png index 2001c831b0..ed016c1eea 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69695044a9afdc82d1a65e0911effcbb660dd550c31d5b8ae1f6a0f674dacd30 -size 50843 +oid sha256:5a0b75e17e467d1a8abcb59f0e490c57e1bc538f10a6900cbff2e2c14994f0f8 +size 47246 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png index bbec3a797e..d4eb3afff0 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acbbccc700060f3511bdc2a44661ecd1442bb8db107c52d8a3ea93085e3cc370 -size 50181 +oid sha256:cd5c0dbd3621d8936b4e7eaac29524fb837007daef330802d71a12c1978d973c +size 46623 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png index f1098ffed9..3466e4571a 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:891f50447359e8697ca26ac27179fb1687851c24156d36006764c874592c7584 -size 49072 +oid sha256:706e54694211c141ad5cd505379f695504b2fa4b793a9aae5242d52f72b9aea8 +size 45157 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 8478c19fda..6224a55c6d 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:e697b923e1e387be4bd8d3a8de0482c9be4a670192f29bb3c5529ff211e9e442 -size 71612 +oid sha256:71815785672b31d061a25b3980ecb368c54d1e7259533fd1989819ae3c922e2a +size 74688 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 8478c19fda..6224a55c6d 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:e697b923e1e387be4bd8d3a8de0482c9be4a670192f29bb3c5529ff211e9e442 -size 71612 +oid sha256:71815785672b31d061a25b3980ecb368c54d1e7259533fd1989819ae3c922e2a +size 74688 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 9462258aaf..4147e93eef 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:4fb4462ae1261dc77869d984152bac7bd810c92a82782c36e2bcc448d3aa6459 -size 72336 +oid sha256:a47734fb0be5195c915373f922ed2fb87f3d89c5c517fefd0fea00cad76bbca4 +size 75418 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 73080a8c95..f8fa0ef3e5 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:eb86dfe02f725547da2b90a4f9390db134f1f27e8c0253a33ae47ed9feb36cee -size 45289 +oid sha256:b4c12cdd10cc2fb443c4dc248ab03740fd68cb8a2f8021225bee5010dd5e8438 +size 45465 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 186d5c4500..44eb24b2f7 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:85c622ee5e7cc5377b07b9df0d6ed856551200af720101f084c56260d5024516 -size 43767 +oid sha256:3aa8259805201e40926ad24b4ac815c38ad03c06df4a931342d954704899366e +size 43769 diff --git a/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_de.png b/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_de.png index 3841e9a8c1..510ef4cff5 100644 --- a/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8a25adefb463d3415cf792c55c2357d0654c9837d9d8c85473337e4da57df30 -size 31673 +oid sha256:2f8be955697c172685ed1abacf1466e6d16be50a127d60fcdd07ffe863a2b280 +size 30422 diff --git a/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_de.png b/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_de.png index 44731d0b48..c14394e142 100644 --- a/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_de.png +++ b/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad35b8e920b273ea94cdcd4e0e3ea4bac5e5d9fda31148c06fe32655b9946b8b -size 30437 +oid sha256:00bd106c1e653a8a9e2c25766c7c7fc3aff62ad7cab95f1c64e7f8a943ccf76b +size 29594 diff --git a/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_de.png b/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_de.png index 44731d0b48..c14394e142 100644 --- a/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_de.png +++ b/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad35b8e920b273ea94cdcd4e0e3ea4bac5e5d9fda31148c06fe32655b9946b8b -size 30437 +oid sha256:00bd106c1e653a8a9e2c25766c7c7fc3aff62ad7cab95f1c64e7f8a943ccf76b +size 29594 diff --git a/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_de.png b/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_de.png index 1031c440de..9fc30c087b 100644 --- a/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_de.png +++ b/screenshots/de/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6df694b16f3474dee2429f9ccf5279bdfbbe23d102911e633937e7f7ef12538c -size 45374 +oid sha256:486e225305ef358a4209a7702e80be03d76a6da4cca3f99106d8b0c652e5f9d0 +size 44032 diff --git a/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_de.png b/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_de.png index 6c25e2f248..d3fc6cd00f 100644 --- a/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3af95d96750feab55d52e066e2201c38224b1b280c9b995fcbfa1a92b2d2472a -size 69083 +oid sha256:828cac2a145d0a4c9f4a585abce862c07ec8f5a96413210c0d3f347b0bc63f8c +size 69449 diff --git a/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_de.png b/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_de.png index 829944fbc8..b686bc6fa0 100644 --- a/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_de.png +++ b/screenshots/de/features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da0280b7c12febde1947781a235d844063bfbf0c7402ceb7585627484135ae86 -size 51293 +oid sha256:711929ee9ee4f29008113163b4eac4646bd73ae0e32ec614caa6dd72827aefa6 +size 49781 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 5db03f0da8..69691a898d 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:3644179c35846aec7a93ee45a6e0ee14ec1ca7795ca270a26cddc7c9813cedd3 -size 40448 +oid sha256:2ba5eceb0adc4b9feb3834f967dac81f1e2b6766934f6aadd54f3be011a98f6b +size 43156 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 5db03f0da8..69691a898d 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:3644179c35846aec7a93ee45a6e0ee14ec1ca7795ca270a26cddc7c9813cedd3 -size 40448 +oid sha256:2ba5eceb0adc4b9feb3834f967dac81f1e2b6766934f6aadd54f3be011a98f6b +size 43156 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 391331f263..e20d4f0a54 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:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 -size 41319 +oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 +size 44028 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 391331f263..e20d4f0a54 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:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 -size 41319 +oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 +size 44028 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 122e462156..2add9f0e02 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:4978ac3b31f5f03fc81846e64c183a857d1f468bc1dc2816b123b92e5b2a1824 -size 67792 +oid sha256:eb7e2b92bd620115488d20e501f4040a042907dcf93d24fc77ce5a9fc178b83d +size 70404 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 6d0b15b0f1..2e8f128b7c 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_14_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:552461f34b6873315ec01cc013fbc3ef4a0796c9bfa0bfd445ef94e7db2d3317 -size 73308 +oid sha256:4ff016cdc3ea47662f224c49fd182cc16ca3bb1e09191ccaf2c418ab93a358a6 +size 74302 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 6e24f8a5ad..7a1dbd9583 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:e2f75acc56ee1d7e6d0381db66cc8771f787e37c51171d1d0e478045759a8997 -size 54450 +oid sha256:587ae11b72d16310df68b70367a7c1dba8611dd252be29c9e403d7dfb216f43b +size 56498 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 822271d7a7..2f90072e8c 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:f1c9d7db00eb7b3eb03328c7e885e8ed608e8747851e3beab4c9f2d9613fb1b3 -size 65694 +oid sha256:54130f6586c04b3eca992f3b3ab08772c3a8dc838800dc0847aaa30fad664021 +size 68285 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 f0e0b4d14e..81fccd7025 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:23ded671d28f0bf1f6f857575603e4f4a63e666ce184aa1cb24d09e868f98bef -size 51149 +oid sha256:be67cbcb3279bb0d0849ee037d4f5310d7c8e56e87906838c0c0588da19ff6ce +size 52661 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 391331f263..e20d4f0a54 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:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 -size 41319 +oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 +size 44028 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 75a0e8a25a..cdc5c1bd63 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:9acf83c1389f316025d5aa95fe3ba43dc871df0cf01cdf364a228090c2873ff0 -size 41709 +oid sha256:7e065760677d14694ab6444762666f2d3af5fe8069cdbb08fdd9ddd3b5d53207 +size 44405 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 08ee8d01ed..0c892f4de1 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:17cc8bd2c275abd9f02a6ec753c05e8af0426c3831ed51d1caa3e3b034ec48eb -size 42318 +oid sha256:c7e3df55da07e3d081934df198f12cefed4599acc8eb8b3db25907cc5062e430 +size 45029 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 5db03f0da8..69691a898d 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:3644179c35846aec7a93ee45a6e0ee14ec1ca7795ca270a26cddc7c9813cedd3 -size 40448 +oid sha256:2ba5eceb0adc4b9feb3834f967dac81f1e2b6766934f6aadd54f3be011a98f6b +size 43156 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 391331f263..e20d4f0a54 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:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 -size 41319 +oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 +size 44028 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 efef76cdd6..079249afcb 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:5e39e5084b58380e734fa88b624e4715489bf01411997a35f4eebcfffa11edb1 -size 39552 +oid sha256:ba49338dbd77ebbfc82d555433e03c9ed222355ac8d716dac5b1677811e55e6a +size 41844 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 391331f263..e20d4f0a54 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:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 -size 41319 +oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 +size 44028 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 391331f263..e20d4f0a54 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:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 -size 41319 +oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 +size 44028 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 391331f263..e20d4f0a54 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:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 -size 41319 +oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 +size 44028 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 700a209aba..fd27a4660d 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:4c89bb4bfe847f0a71ec7b74627b21f9e20245472d16ba413416eb3705d5859d -size 50721 +oid sha256:e0ff3fab68e7412f4007b2fbed67268f114064097e3256d051b235cee3350296 +size 50172 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 742c8b1be9..13bedfc6b3 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:785114ab0576fe9a69fcffa7dff1252ea660345a5706949e461878859dde0c33 -size 47362 +oid sha256:deaffb7054cdb7dff2437f94f19bbb7a135e468d205fd87df9fd24a863483008 +size 46802 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 894b66623c..1292c01a4f 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:ada44308165eefb690684726100507bc2c20e1d363a3555f7d8975c6645e0536 -size 40144 +oid sha256:90ec71c1f37b2cadc100c3ad14fc03aa1abb76881834a97c9d7db9deef49456f +size 40043 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 62f0a77598..cdf64c352d 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:1ae69f5a8fc884a2ca02d8c84e9342b9633512de78348a0bb2d661d5735e708a -size 63185 +oid sha256:73542eae97679631df08f0e505ada9a12325ca5544eae4d37dc5c0a0ea4c4939 +size 62398 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 3fc33a270d..55345f0018 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:6930558f6c4cda30d6a83fa320a7a17d4c45db0bfb2e9bf21b13701d047e76a2 -size 59611 +oid sha256:12537f5ebb752dd40f9ed048bc30669f78e8fdffd3bba3c4c4c7f34e8aee1f6c +size 59311 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 388cb2f945..217548fbb0 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:57dc90ddefeb2743ef616ddd11e5a9cb3847d714e2f452e2e58f7d7cc02c3422 -size 41780 +oid sha256:0cfdcaac6e0f1eaa0bd95e26fbdebc698dd90cd0570bc757755ea8bc8d66f82e +size 42170 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_de.png index 58d6c1f136..b9e27d64de 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ba6580e7b5cf07c6dbd067ec863dcb485a3fbc7d03d93dbffd8a6c2d337b058 -size 29129 +oid sha256:4f91b5e9e379a6053f2dc204edf67dcdbc3aaea7b0ef055360cb8e1c749a862d +size 28994 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_de.png index 2dd817a0d9..2ee8c1d373 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:054a9b89b6c4007b839e53190b10938a7dc5947badad88876be243e1386628a3 -size 61787 +oid sha256:beaf80641fe9918dc76626709feaf7e4f20445fde61ca26dd867eb77960d39e6 +size 61646 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_de.png index 2dd817a0d9..2ee8c1d373 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:054a9b89b6c4007b839e53190b10938a7dc5947badad88876be243e1386628a3 -size 61787 +oid sha256:beaf80641fe9918dc76626709feaf7e4f20445fde61ca26dd867eb77960d39e6 +size 61646 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_de.png index cc0df3adc9..08a5405021 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ffea6362670777720ad844d424ca9fa8ca9a3a79f26adbdaad1b28adbfe18d6 -size 56453 +oid sha256:81a6bbb9b125f32ddf968b39589a50f5c86f57e0ce74f64c4bfdeeb0a15d1044 +size 56320 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_de.png index 84155f40e1..a4810cfa49 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:323f616c91310b48da0170fddb7c7ad68edff7ffda48e933cf20e9728b8057b1 -size 63607 +oid sha256:732945a494e17c01ba15eb2d125c451dd24fe0d372e4f8c326870c00cd5919a9 +size 63468 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_de.png index 84155f40e1..a4810cfa49 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:323f616c91310b48da0170fddb7c7ad68edff7ffda48e933cf20e9728b8057b1 -size 63607 +oid sha256:732945a494e17c01ba15eb2d125c451dd24fe0d372e4f8c326870c00cd5919a9 +size 63468 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_de.png index fbd2710432..6052cc869e 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6e6da70984ea50474cf4f12c78ca8a4df196e92bdb77bb9ccebb4e00ff986cc -size 30460 +oid sha256:9f50dff310fa256e245c367ff86925b5836a01c15c291693fc63cbd252218e98 +size 30303 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_de.png index d016666d26..d1472d9bce 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3c2270c8ff1643956b0ab4fcd066dd4509b0fa175ae8652e83bde5c1c3aaa33 -size 64089 +oid sha256:c1b686819cafac3ef2b162303af90ee7a3235d9710bbfbd32b55eb32795344ed +size 63939 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_de.png index d016666d26..d1472d9bce 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3c2270c8ff1643956b0ab4fcd066dd4509b0fa175ae8652e83bde5c1c3aaa33 -size 64089 +oid sha256:c1b686819cafac3ef2b162303af90ee7a3235d9710bbfbd32b55eb32795344ed +size 63939 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_de.png index f434d2afca..e478d3dfd5 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16423874fda81c259bc18949f550ccd0cd2f56aff89304fda784d51d02e175b4 -size 59036 +oid sha256:2cb8223671b7e668dd25586e72d06eda531b8f73e25f2a0c8f58c8a20e2bed69 +size 58885 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_de.png index eab0ec0568..ca3ca6fb08 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7fb8ec382a876de47b2c1fb89de638bd1b6fd44e088e7fed4eef09c59f8eade -size 66082 +oid sha256:9ecec81c744dea2ba5adcf8154ef01a817066e7efeeb815e97db0b387078d3ab +size 65933 diff --git a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_de.png b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_de.png index eab0ec0568..ca3ca6fb08 100644 --- a/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_de.png +++ b/screenshots/de/features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7fb8ec382a876de47b2c1fb89de638bd1b6fd44e088e7fed4eef09c59f8eade -size 66082 +oid sha256:9ecec81c744dea2ba5adcf8154ef01a817066e7efeeb815e97db0b387078d3ab +size 65933 diff --git a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_10_de.png b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_10_de.png index 94df28a991..379896cbeb 100644 --- a/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_10_de.png +++ b/screenshots/de/features.space.impl.leave_LeaveSpaceView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a35bd47b1c72dc9bd8df40ae31737fa86826f29fa895b8516d156261bd678fec -size 37731 +oid sha256:860a35adb3a3ed607050890e3a7c246a952162ab06de7a64c7d19dad27cbe94f +size 31909 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_0_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_0_de.png index df387ee482..9ac2444e4f 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_0_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8ec051c3ff5258f937b80e879d5120cff573558418e022df0604c85ea0d20af -size 50027 +oid sha256:f8804204dff0d36762b0c02e9503f2e06b6be257b9e1c0c2af6df0c0fe2855b9 +size 50289 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png index c3ddaacb75..36817c5f75 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e253dbf67edbe2047726be540ca8dd96a92623fe786f4c642ce369779b2a20ea -size 51127 +oid sha256:a69c330592d208be768c7f607bede843483a92140155c974ad4a84116ebd142d +size 50380 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png index 03e3a3e58e..40e51c9537 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a11958a3fb1af0e4ab037fc47aca888d9d14fc6fa692a8409766071629e55631 -size 52246 +oid sha256:428725b17dde8ae30898299043e2d5e03ce63f516c93e440e660823fabd43e4a +size 52036 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png index aa75305737..806bb43ddc 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70972a7bcb6546370e820cd8e6dd56f27d8ebc42974d041fa4d028520ee1407d -size 61119 +oid sha256:ccd76b6702435a24f67ab28adc60b462acb2fb430daf87b6ddc53237940bb29a +size 61987 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_4_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_4_de.png index 1fe2580582..75087f2a67 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_4_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e59fa963aa02f2b621c9dacb4dfab56dcca4fde294d1cfecb31c83496dc6448e -size 61761 +oid sha256:524f0d29e59175ceb996b81b98625e168033077b12541f8747fa6c6082097cdd +size 62629 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_5_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_5_de.png index 50e51d4b3b..8a71650321 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_5_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc94524eb19fe2878558d3cef5da93247f83373b62175115f7d6ede815b2a404 -size 57045 +oid sha256:e338d699f2cb5d4645742e9dff3dc7cd3dec20d2ad75821bd3fdb3fb8e917a96 +size 57250 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_6_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_6_de.png index bbc9cdc6eb..bb7279e63f 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_6_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16b7d04c97f37253169364cb66712c70cab46548833b9a4249cfa8e3c56747a3 -size 32833 +oid sha256:559df2e03be1e673f1bd3d8232d173fba53cd2f90f5db3dbfaa7198bac9c1f9c +size 33506 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_7_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_7_de.png index 6f1d20b710..f41021a302 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_7_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:980dbcdfea227a2209e7738d846505f3b08ec3ca1e553f44e1a94cafec1127c5 -size 33461 +oid sha256:63d47784152edf52001321e94d57a9724647c33c1b3cb960b3ad7bf399963a39 +size 34019 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_8_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_8_de.png index 744202fb4e..624b13fff5 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_8_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4bd5b6d993849919a38fbe546673441e455def888463e9bcc3377404fc9adda -size 52228 +oid sha256:f71d4b804cada9b0a6ca00a91a423095f8c1f2b15908b6a63e9250a079268407 +size 52373 diff --git a/screenshots/de/features.startchat.impl.components_SearchMultipleUsersResultItem_de.png b/screenshots/de/features.startchat.impl.components_SearchMultipleUsersResultItem_de.png index e86f031a8e..34d3e5d494 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:fa263cc9cfb7cc47dacfa4ea65a9f4e8660e1a17df61e357d5cf76928dd89100 -size 90714 +oid sha256:993191e7db24d10111a3b7c22c0e192e28e9a3e170edb57de14ec205de73cf15 +size 95383 diff --git a/screenshots/de/features.startchat.impl.components_SearchSingleUserResultItem_de.png b/screenshots/de/features.startchat.impl.components_SearchSingleUserResultItem_de.png index 3eff580ee8..8d1aac76c6 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:caa936c590efb400eec2e3d238bec4e48b81883371057f34badf0abb1644554c -size 49654 +oid sha256:93b60fdeb5bd6fbf7f3ab3eca81f2109b02101bec9a8c812ceb02ac8d19840e2 +size 51866 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 1230b616fd..502b87bbdb 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:fa14bce431debf9d9a64c4a47bed9d89730fb21d480af186dc2ac53a9e52f1af -size 31785 +oid sha256:f7884a15f505913ba13dcd99320b1b9d26cbf1e8f15eb051e65d8fbedc1a828c +size 27673 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 14b7d796bc..33479d694d 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:0f4fe803d3318a31919099958d793ecb4f5eae7109596712de23c89e6c982ea4 -size 20145 +oid sha256:8cea006dcb900e70cc9f4be90b9144e340768e7d23d90932c99422eaf8ea96fb +size 21203 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 358883997e..e4c6ad93ff 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:a99cb9da94e1ea3050ccb1e9c32380fde1b5e55ea806c4c2aa5f7bc8c5d0b0fd -size 30750 +oid sha256:41ddc90c4f22013037b7786f2b91e35dd2db7329d43f12cb72f8cd1474e89d3c +size 31725 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 fc81f7c584..3e3fcc2526 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:af238ae63b675069702e65b52c1e4b272294aa0a2c971a3394a67535ee89e45c -size 51165 +oid sha256:1a17fadc85a373556a338cd4c9ceaf35432b1853e06641237e6d3f4bf7cd0979 +size 51495 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 3bbed27e43..75e7c2f592 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:26cbf2c908b89e8cd2daa1a35cfd58dcea951e03a2ecc2334af7d8241d038254 -size 41339 +oid sha256:44855b0887ce9ccd5a0302c8d11ba6bf556badaefd33f897c5635d49cb44a99e +size 45071 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 new file mode 100644 index 0000000000..1230b616fd --- /dev/null +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa14bce431debf9d9a64c4a47bed9d89730fb21d480af186dc2ac53a9e52f1af +size 31785 diff --git a/screenshots/de/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_de.png b/screenshots/de/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_de.png index b3ca4cd447..f7f4fe51a0 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:234efe683bd6bc802f2a9502faaf6969cb5ad373a1128af8dacc99969d9c10bb -size 25692 +oid sha256:f9d20be83c90e8e0a54f59bd7b16cc914b080cb15cfc31b682d43724c3e3ad33 +size 25315 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 5b11ec067d..ebf998ba32 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:35ac4bfa2e7bffba17448459c52098400f421fc3af55a7de16c24f3f293d0d42 -size 36464 +oid sha256:aa5d7bb68d549eab01750a043f0c844cf9ff2609e813fa6be5170f7e47da04f4 +size 36872 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_9_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_9_de.png index 952de3033f..dfa9eaa2f2 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_9_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f5a232c60af123ef006af7ca44bf43acd21dbc8b1f4568640eb7ec78e2378e8 -size 34580 +oid sha256:540458fc3c97157112560ac44278c57989fd7db20f2cd1779f2a99b1f666f7d2 +size 33938 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 4cfccb8015..fc2d5b8229 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:00d1fe85f79d7d25867287c50e50f2d0b37b52fe93c4cee70b17da485c912639 -size 30380 +oid sha256:b6d6cb69eb5d310ca7dae9127015fb3443325b16322c94958c4bf947fcc70514 +size 29438 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 ce9e73fd68..ce7583ad06 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:70c06787bdb7614fb66fb9c16a9703fc572500cbd4b7ba48d806b6c6bb68b649 -size 44169 +oid sha256:84fb184c49d26ad7636588885daf3c9169209975a5b0e2f1c9ffcea6a3875683 +size 43302 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 5c5957c19b..95995db201 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:9bcad1fdaeb5596e17cddad4498d58208eccb7dcf38efdc7434bf82badffb5ea -size 43767 +oid sha256:2087ce92d6d532a71a1dcc3f0824a34107316030e25ce58d46696675ddd6f264 +size 42907 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_de.png index d617ad8f81..673bb92edb 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37b686d30cc5cd2bf73ee82736cdcb0dfa5cac04401a7ad9d271b191296d8094 -size 36911 +oid sha256:3966e491e38c1c3a81faee839282b66260a1444c8878f03e6ce121941ffacb72 +size 36710 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 5673458928..275f38c3f5 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:3ffba2cf2bdbed63ffd3f378a9e5b2a8c08bc316c500d7c782dbca8152c07224 -size 30449 +oid sha256:b2e955575444e16c332ab1e22cc121566a4f8744b8e0a01c69c5f3a9ce8ba033 +size 29507 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_de.png index d617ad8f81..673bb92edb 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37b686d30cc5cd2bf73ee82736cdcb0dfa5cac04401a7ad9d271b191296d8094 -size 36911 +oid sha256:3966e491e38c1c3a81faee839282b66260a1444c8878f03e6ce121941ffacb72 +size 36710 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png index 6a392c8059..6463867d7d 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48ae8baa9ed17971852d6d6c0022196ca076d6df67ebcf16046929ae11d91e0d -size 50565 +oid sha256:f85a57b51f5ac9d7165be8d83c414b8a1fce87f2bbbf4a345f145f01ba80e3f2 +size 34420 diff --git a/screenshots/de/libraries.matrix.ui.components_CheckableUnresolvedUserRow_de.png b/screenshots/de/libraries.matrix.ui.components_CheckableUnresolvedUserRow_de.png index 5fc8b4a8fe..88e163b840 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:81e2d991659061d40047373c3b3aedff5a4bc2b42c2b1355448b73784891b2f5 -size 109822 +oid sha256:e4d6fb36f1340276b3f525505e8a51245f63a1fbe8637ad33c9c3c2ef793679d +size 114204 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 6b49ae7f1a..74fca61a42 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:4484ff8dbba915c439270cbebab45c503d94dc82c7641a674153d88aa2aa3143 -size 28017 +oid sha256:a9da4a0d12b26e2bf2bc3c6a0b032d462ccce782ee6ed5a62c7c32a7c2d4e0a2 +size 28400 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 752973ddb4..a1c8e06dda 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:402ca214ecc34d4a5a5faf7583a879ab26e6c64b86904034dfa050a27a7b7a2d -size 38789 +oid sha256:fc315b17a2234172e3701606c1409b6ab1d551cc58e42ac7db80b2e76a852c69 +size 26784 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceHeaderView_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceHeaderView_Day_0_de.png deleted file mode 100644 index 7e57514e60..0000000000 --- a/screenshots/de/libraries.matrix.ui.components_SpaceHeaderView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1ef090ea6ebf5245e8542ab2f5419b0b16656bb19080246558022ad10c201871 -size 63969 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png index 8063fd08e9..47ae5e64e6 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c54bdbee226f0b32754e7bc8822dd6a722d096b8b0bea671b7b3f7e63e13cb2 -size 18810 +oid sha256:5f245cfa43b2310ab533005012debe347d196b4c177e696ff4399c8431397113 +size 18057 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_de.png index f18a0304a6..d8a2dd91e3 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6918b0eb0b7aebabf8017ea3f4d6facd7ee40ed10e4ef5def22209df9d722296 -size 14681 +oid sha256:c68172fc235ff99b618910a4312d650da867b4fe6cd91d5a650e4b9ed2360948 +size 14914 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_de.png index f8cbcfc1a9..befe1594c0 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d05166fbb55144719d8f304a2055b4d586e31768e0a840539b6584a01c8a9df4 -size 14310 +oid sha256:9b736f4d8865b2607f549ede17418fe9ea86e523608ce7c98b97dd846d7cb9c3 +size 14543 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 f89e8cf1c5..f7488f52f5 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:f02b4fe30d6ab57d618c925d3a110b5ca8f299717032b1380d91437acda62b7b -size 11633 +oid sha256:d7d0c9d5ec4e405c9abe3f708359a1d0305d06643e7862b69a44111b91adee29 +size 10236 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_de.png index db8c3a14ee..11578a4a99 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7d114b3db4b9c915b07000f679b99b96a3dc823e8a000da40445ffb91b9a001 -size 20341 +oid sha256:f448d9d670a8120f3ffbc11a8fbedbcfcaf2ac7ae9487d27222ed949654e2029 +size 20577 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_de.png index dcd39d5329..f48b52f0f0 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a77b495523c1a7cf42f0a12ace13a897e080368317d76e40a2997d5b8d3b7fa6 -size 20033 +oid sha256:b1daf71801da8ef47d3ee5184391e896f574486b4512d3700552fa4dbb4fb475 +size 20267 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_de.png index 060ad3df9d..130ecb8da2 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab8ef2ba28427bcd185c6055edf92b4fa187df547c52cf392d8b37c2375e47b7 -size 13943 +oid sha256:069b8f17330883e7161292ea46765d2c81c56fe8e3cef4a8a1a89968640f7c8e +size 14176 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_de.png index 327ea308a7..7cba4e51b9 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7dbf9bb99bd112618baff601ce3304518baf0cc8401d6b984d268264cf15007 -size 33349 +oid sha256:ec2674db1e325e8009a9dbce9e012462a0508cadce19ca56e4b73cb295b0bc33 +size 33581 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_de.png index 8a727c7381..9951601678 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90703a90d1914820f1ebe396b80c57a87c85298fb66c092536b16e9b209ae291 -size 38296 +oid sha256:a6dccd98617c8fb11df558e4d2c152e9e012f471a102edf1774f2c5f5a0e32c5 +size 38513 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 6aee76799c..4dff07dca5 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:1dde928940515398c089e258787afca156fd1dceb9b80559c339e94e97864f12 -size 12052 +oid sha256:90cd3c57526f2da48edcbbdcf354453e19bad5310052b46150f3ac55953a1f67 +size 10743 diff --git a/screenshots/de/libraries.matrix.ui.components_UnresolvedUserRow_de.png b/screenshots/de/libraries.matrix.ui.components_UnresolvedUserRow_de.png index 25ad5c2356..250826b00d 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:76955fb8324747c81687727bbced594f00d2c2610d91474123ecd6ced14394a4 -size 70297 +oid sha256:a7179346d56f1c4772169e344f1e5f01b42a5cbb86093dc6e14728f970f5fc6b +size 72739 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_de.png index efb7231b4f..b14856438a 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c39fc1f4b149531f599f5ea69087685f63750f584f53fd98f9457f6e8e1a026d -size 35492 +oid sha256:4c50e2f2892deb269aab50da2ac188df46cf00a4d1c242ea748cff4fed06058a +size 35461 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_de.png deleted file mode 100644 index 4ecdcd8c5e..0000000000 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a5bfe153d49c2cf8cfb0305034bd062c3196f11e89e745a3da9965d1148870a -size 47632 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..b62f148452 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:a3ab3bf722f90727b238a6d6a51c75ce40b1074343e30dd714c3dc86977e9c6c +size 45120 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 deleted file mode 100644 index 5fa5394165..0000000000 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5e2325778fc5bdaa5ffd421f77cb83d1927ad4ad6731337b4878a99d3fbfe8aa -size 46053 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 deleted file mode 100644 index 1e6c6d88eb..0000000000 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8f845e0783b6f09ef7b0472f22465e8e5b086102d711e468f1466df45ff928ed -size 50410 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 deleted file mode 100644 index 79f832dcc3..0000000000 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a2917a353e78569a92d5d276e99ebfce334ad8848d488f58312abb9d7bcf654b -size 33327 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..9ac592fc55 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:fced21fbe2a7c8d0fcdaea9c06c063dd747080fcf7cc35f42257cba68bd0cedc +size 45022 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_de.png deleted file mode 100644 index c4822fa433..0000000000 --- a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c2c35cf93948bbc2c344bc678db1159e77769577054bee9ed168cd07ccea5de2 -size 28202 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_de.png deleted file mode 100644 index 189dc5c26e..0000000000 --- a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:762de4ead1258ac712f31b91586597f78487653c5ab3fbf14c7f92835e38a544 -size 30733 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_de.png deleted file mode 100644 index 8ffa83e0e9..0000000000 --- a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a6e4532edfb990b61d5fc6c30e5e054ef9de9c417ab39ed9b8a87d7936e0536e -size 8242 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_de.png deleted file mode 100644 index bf7e3395f8..0000000000 --- a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6cb73f72e0e28fd731dceeabf2a1f2b03e194f59445e7b7b0757ef1674d3849f -size 199972 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..0f7abca2b0 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:013e99fac97e20013d15d780a9f9b39663fe9979378c923c2ccb29443f087212 +size 43323 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_12_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_12_de.png index 6c364f2a44..8c73c6097e 100644 --- a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_12_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91045491d147d4543f3918f7862426fe5eac5f5012d581d9dc03648f48298120 -size 35572 +oid sha256:732355669f5a2e1382aca0c4c39eff4416259551f7f5e6da9a43c07f6420e9ab +size 35037 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_2_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_2_de.png index 24779ec32a..b83d3f8641 100644 --- a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_2_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f18aabd71856df14464096c7679850ef59a501ccb72c451606e8929abe8e8493 -size 46656 +oid sha256:acdf6d5164b6af3932a9ea1ff8c7a1fb8a97e3829ef1cf8e27cd691fb4b0b7ea +size 46192 diff --git a/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png b/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png index 13aaccf97d..92f956925d 100644 --- a/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9690aaa942b7314d5c0342eb2f50112b3eb27393c47fdff451ff3f7501e96c57 -size 52058 +oid sha256:40f902114622ce212797bd9c0a7cf8fa9d465cacf1f32af99422558b35907d02 +size 52217 diff --git a/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png index 6b9a41d3f7..c36a2ce217 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bed208bbf05ccce0169888e2c0ea44c2cfaafd56fe31af1ed6d0f61ac24dcae -size 57954 +oid sha256:e7326fadd145ca624810e858cc3413fb56a25f875854b7d2e75cd7e1d1b4134f +size 58018 diff --git a/screenshots/de/libraries.textcomposer_TextComposerCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerCaption_Day_0_de.png index 4b54cbd8f1..8efd9aae7f 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerCaption_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerCaption_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd33857c5c2c12ff5682c23fc10c351f7cab15631c67a915574c97834f6a4c98 -size 43353 +oid sha256:cafd41a70160f81c647bb908e030609bd50003a54a89e1c7a58e5a7c6b20feda +size 43321 diff --git a/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png index b2657abb78..f9cbe155c1 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cfa7073fce357505cb0aff9a44fa93d3c1ed36801c866d8c282743dade3bebb -size 55042 +oid sha256:dbb50973fb9da0bb43708e3e608de3a22a301a2dc0d8c699bd9e0f59d5b44f20 +size 55991 diff --git a/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png index 7aa4a3b10b..c6d05fe805 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08e8c143ca1006a997b90d2b91a54c56d7e6d4f75fee8fbab5435772c94c75f2 -size 65057 +oid sha256:987e6f677fe5412dcd5c94a883bf5966b18d048b31f189614ae2763823e319e9 +size 65017 diff --git a/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png index 7124c2da84..92f956925d 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a92d298fe4665f641db42908bfdd884f6439e6ba49fecb58e3b0d4ffd365f65 -size 51880 +oid sha256:40f902114622ce212797bd9c0a7cf8fa9d465cacf1f32af99422558b35907d02 +size 52217 diff --git a/screenshots/de/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_de.png index 2b6c35435f..42a444fa21 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:311b767a0e3406c6f6e46045b573ccf8033421008c39bc5a95f25d55f92c26d6 -size 63391 +oid sha256:0616572000748e8483c377e64439159e00d5a836459d4208122b08b261346d29 +size 63305 diff --git a/screenshots/de/libraries.textcomposer_TextComposerFormatting_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerFormatting_Day_0_de.png index a883181de0..ee39ef7d5c 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerFormatting_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerFormatting_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e20f3e52edb21bc7ce9ea63724604d59b749b841855a0b1b3a124e262d9fcd50 -size 51125 +oid sha256:52b4ac22573e9ce47198277607cab5a5b9deec09e2adc88b92fac9a46ce22b7f +size 50963 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png index b473ee22c8..77f972cdd8 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:221bcb0d8c23c286d330fe945d90cf864a17b8b6b3728e5fad98781353aa6ab4 -size 72954 +oid sha256:49de6907472a673c5b79ae0f20a9939f9f6786fc757b9e1ac96077be34557d06 +size 73249 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png index b48cc48692..ac61252e96 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17ff7c0207c49e50e950f276d63a58c1837b98048e5300278962fab0dd8d4184 -size 59481 +oid sha256:29a442ba2414d64a550f4bded4835e11daeff0bd55031689af7072fba0fadcfd +size 59788 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png index 7d95518cfd..e9b333dc05 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66f5fc6ad92fd7fea6d2c306cc666a4587cc1cdbc3aa98b45142cdf0c41e897f -size 72530 +oid sha256:e5493a95039dae9babedde3214acbbff01c7b53a3b454251f234c6cdb09bbd35 +size 72797 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png index 8f5381f932..03a3fc0a40 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f447bb8d44aa37b72de057afd9990f084efd60a11a2eb780c421a079e443903 -size 81106 +oid sha256:e817846aa9983e8e38ba6c8ff29ec205636e7b9a72b461215c16eb06e22ceb0e +size 81614 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png index e38e38a198..bef0dfd03d 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0581ab71b5d91f4308fce842d2374168cbfeeb92e60658e4cdbf2e62d54e0eaa -size 62378 +oid sha256:ddf5dbaf0ad4247513d38a34e89897d099daa8e3f6c0bf9495ac65dad0771f34 +size 62532 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png index c670eff478..9267d1cb6c 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e181b89dd2bf6ed1872e6a0fca74f7e335fc630015e4073b055da7a51426fd0 -size 61139 +oid sha256:35b64678b72a2e2433d278f717d859dc6f53414db565ed325604e0911e8a7e4b +size 61375 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png index 58301a23b1..a43cafd104 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cb0396eb3ffc95f88272f788542a7a1c6e3f7e63c76beb6bd7eddf46f7dc41e -size 66635 +oid sha256:59b8beb90fbe005555a4de1d67230362623ab9b715796abb3a223f028bf89d5a +size 66981 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png index 6c68e65089..1f186b58be 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e975a70bcdf6e57b0df77d9e301252a934ed70bd48f0469433434246a668a023 -size 89801 +oid sha256:0ad07ef8db1754dadbca19207273bbbffc83a591b615c22a6d38fcb8322af072 +size 90155 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png index 8588939a09..558f92c29b 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bec919f02167d8c094f84e0fba555000b1fc8ab69fcee16bd8e2a6ccf3fe8af4 -size 60444 +oid sha256:e130d13f832caaa3b4016c2a915610bde46f0d34e7b394d52ccfb96f367afa74 +size 60683 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png index 824938172d..0db525b4a1 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b42a72b8a0c47c86f86c71ba6153e4fb96e6f7fe4e4c43d4dabd6f3c5bb0ea1d -size 60510 +oid sha256:72db00abddd84b586596e1dc61fe89b27096fbc08ba82d23eaf703c5c83173fd +size 60746 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png index 23cf3ec660..b14578a682 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7a065b41b1d91d778b802bc9ba58a9491efc339fc08365ee93a2ce546ff630d -size 69115 +oid sha256:926284ff1d11c483707b568100fbdc8382a1a61ba146a0ef23594089bb3038c8 +size 69406 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png index a421bc4928..340cea1e36 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecc34684316796fbc9ca8ef51c51dffdd208cdba1fa3094bf9cc78032ff69c5b -size 59948 +oid sha256:2cf0dd833df3ace9700622b31f64513a3c76908d6651dbb0ffcc3b5a6a4dce9e +size 60201 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png index 995c0bdbef..ef613cf899 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c2c8b262ad96121c40ada406ff69eccae562dde53e6c443e998a1e9298af176 -size 72670 +oid sha256:164558c7d8cdd701a1d11e4f3df0448818a47eecb06024904e14c29651800f56 +size 72929 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png index 664c684c76..a16f0aaf45 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fea796dd9d77e3fdd8299c466ea7e288416139820e3d63160e14b602f8357fad -size 55744 +oid sha256:b801d74b3feee574ba81d9d2e270090c86e3c007daa9b96674c752e6428a5d9e +size 56175 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png index d0f110d40f..39c6f3f77f 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0144461bd7e0076ef528a469ac99c7c8ca1d3473dbc18ebd635d430c3a2f13c -size 71061 +oid sha256:e088310a4666c06e55fa88ef9d209e24ff681f9caaf148b1f1cbf4e0a8954206 +size 71274 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png index cdbe3c1024..a906ed4d88 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c91ce0f1fc215deeca1e07c92a25ea1ce3c67c7e22c2ec3c8a611b5ee8e2bec6 -size 82226 +oid sha256:8ffff1b75987e4b692fbec95fdc6bac243fdfb741e24d56f2e0f3b05c1b9a9a2 +size 82793 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png index c0c96ebe50..6ffea11e25 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7537431b942cf13953936fc7be06a0e269e03ac601426e60e0de3646b4848250 -size 59027 +oid sha256:17b44b1796d438eb804fec2aa1935c9ef0fbac16b90c55ee57ae0393862391c8 +size 59342 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png index 908b4f2e78..0bcdff0b59 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0490cb7ba961da251b82da2d4e248eb5e930e5cd853a0c0e4c184863483c1be3 -size 58094 +oid sha256:467e422cef79f2be11df55814794472113b434822434301e52bdfd12c094deae +size 58453 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png index f34df6b6a6..d28b54f3cc 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7bbd1bd20c2f1198e8e9681ce20a162586d4240ff8e6408675abad364f1e544 -size 65417 +oid sha256:e15f573b4d081b17f91ad2e19cc4920063dad46755217145fb8b13288eb2ad3b +size 65754 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png index 9c1c251e28..1e87d303a9 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a05a648bffe46f29a377bbf4ad548df153e3c17a0e8014a594d8314d4b85cf4b -size 101198 +oid sha256:d80a1a50b289631c432e1247af2ec1a41a9cb233f4ef9c5f6c964784113ff401 +size 101387 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png index 80150353ae..7903b1ba42 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cc0e4d504ebda2d86e0c7ae557a89587d68957bacfcec9f918f740ab9ddcbe8 -size 57121 +oid sha256:157c70292051f5dd737f309b2bdadddbe6cbf7ec3b322a3c47b937edc4df72e0 +size 57461 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png index 51f4051085..a997076a87 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b657f75efe8e75f2e8f0ca45ed0d9ecb62400e3b214764edc2e1915812742c3d -size 56980 +oid sha256:44df9e6774110798c72042f560bcc344a5b292f11cba5893190ad78513a8d07e +size 57468 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png index abfdbcf57e..b198b87ff5 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb0220077eb2469994f899d9819baa3b4422525c306159f651f502cbebd24150 -size 67414 +oid sha256:8323ee0e5869c5408cc17476fb603af972fa570d1e6896bc063ce401f4d1582f +size 67617 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png index 96f810d9ab..5515d78ee1 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e9c42a7caed334e3da02c82d234b61ba95216d65e593ac8461c0649a0b032ab -size 56467 +oid sha256:31e974364b144d6270b890bf7293ca84a8680105d3914d5d6d2c1fa766b463a1 +size 56853 diff --git a/screenshots/de/libraries.textcomposer_TextComposerScaledDensityWithReply_de.png b/screenshots/de/libraries.textcomposer_TextComposerScaledDensityWithReply_de.png deleted file mode 100644 index eaa36d2c24..0000000000 --- a/screenshots/de/libraries.textcomposer_TextComposerScaledDensityWithReply_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:181ce0a2bf5e6eb95a7726bdf26540f2320e5fb6bc4d86b17f9e027314d79f0c -size 16262 diff --git a/screenshots/html/data.js b/screenshots/html/data.js index 119bc1e013..df612e28d9 100644 --- a/screenshots/html/data.js +++ b/screenshots/html/data.js @@ -1,100 +1,87 @@ // Generated file, do not edit export const screenshots = [ ["en","en-dark","de",], -["features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en","features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en",0,], -["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20595,], +["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20553,], ["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_0_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_0_en",0,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en",20595,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20595,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20595,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20595,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20595,], -["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20595,], -["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20595,], -["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20595,], -["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20595,], -["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20595,], -["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20595,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20553,], +["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20553,], ["features.login.impl.accountprovider_AccountProviderView_Day_0_en","features.login.impl.accountprovider_AccountProviderView_Night_0_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_1_en","features.login.impl.accountprovider_AccountProviderView_Night_1_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_2_en","features.login.impl.accountprovider_AccountProviderView_Night_2_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_3_en","features.login.impl.accountprovider_AccountProviderView_Night_3_en",0,], -["libraries.accountselect.impl_AccountSelectView_Day_0_en","libraries.accountselect.impl_AccountSelectView_Night_0_en",20595,], -["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20595,], +["libraries.accountselect.impl_AccountSelectView_Day_0_en","libraries.accountselect.impl_AccountSelectView_Night_0_en",20553,], +["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20553,], ["features.messages.impl.actionlist_ActionListViewContent_Day_0_en","features.messages.impl.actionlist_ActionListViewContent_Night_0_en",0,], -["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20595,], -["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20595,], -["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20595,], +["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20553,], ["features.messages.impl.actionlist_ActionListViewContent_Day_1_en","features.messages.impl.actionlist_ActionListViewContent_Night_1_en",0,], -["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20595,], -["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20595,], -["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20595,], -["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20595,], -["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20595,], -["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20595,], -["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20595,], -["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20595,], -["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20595,], -["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20595,], -["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20595,], -["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20595,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_0_en","features.space.impl.addroom_AddRoomToSpaceView_Night_0_en",20595,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_1_en","features.space.impl.addroom_AddRoomToSpaceView_Night_1_en",20595,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_2_en","features.space.impl.addroom_AddRoomToSpaceView_Night_2_en",20595,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_3_en","features.space.impl.addroom_AddRoomToSpaceView_Night_3_en",20595,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_4_en","features.space.impl.addroom_AddRoomToSpaceView_Night_4_en",20595,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_5_en","features.space.impl.addroom_AddRoomToSpaceView_Night_5_en",20595,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_6_en","features.space.impl.addroom_AddRoomToSpaceView_Night_6_en",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20595,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20595,], -["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20595,], -["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20595,], +["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_0_en","features.space.impl.addroom_AddRoomToSpaceView_Night_0_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_1_en","features.space.impl.addroom_AddRoomToSpaceView_Night_1_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_2_en","features.space.impl.addroom_AddRoomToSpaceView_Night_2_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_3_en","features.space.impl.addroom_AddRoomToSpaceView_Night_3_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_4_en","features.space.impl.addroom_AddRoomToSpaceView_Night_4_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_5_en","features.space.impl.addroom_AddRoomToSpaceView_Night_5_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_6_en","features.space.impl.addroom_AddRoomToSpaceView_Night_6_en",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20553,], +["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20553,], ["libraries.designsystem.theme.components_AllIcons_Icons_en","",0,], -["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20595,], -["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20595,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20595,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20595,], -["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20595,], +["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20553,], +["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20553,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20553,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20553,], +["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20553,], ["libraries.designsystem.components_Announcement_Day_0_en","libraries.designsystem.components_Announcement_Night_0_en",0,], -["features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en",20595,], -["features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en",20595,], -["features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en",20595,], -["services.apperror.api_AppErrorView_Day_0_en","services.apperror.api_AppErrorView_Night_0_en",20595,], +["services.apperror.api_AppErrorView_Day_0_en","services.apperror.api_AppErrorView_Night_0_en",20556,], ["libraries.designsystem.components.async_AsyncActionView_Day_0_en","libraries.designsystem.components.async_AsyncActionView_Night_0_en",0,], -["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20595,], +["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20553,], ["libraries.designsystem.components.async_AsyncActionView_Day_2_en","libraries.designsystem.components.async_AsyncActionView_Night_2_en",0,], -["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20595,], +["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20553,], ["libraries.designsystem.components.async_AsyncActionView_Day_4_en","libraries.designsystem.components.async_AsyncActionView_Night_4_en",0,], -["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20595,], +["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20553,], ["libraries.designsystem.components.async_AsyncIndicatorFailure_Day_0_en","libraries.designsystem.components.async_AsyncIndicatorFailure_Night_0_en",0,], ["libraries.designsystem.components.async_AsyncIndicatorLoading_Day_0_en","libraries.designsystem.components.async_AsyncIndicatorLoading_Night_0_en",0,], ["libraries.designsystem.components.async_AsyncLoading_Day_0_en","libraries.designsystem.components.async_AsyncLoading_Night_0_en",0,], -["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20595,], +["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20553,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_0_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_0_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_2_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_2_en",0,], @@ -104,19 +91,19 @@ export const screenshots = [ ["libraries.matrix.ui.components_AttachmentThumbnail_Day_6_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_6_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_7_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_7_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_8_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_8_en",0,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en","",20595,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en","",20595,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en","",20595,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en","",20595,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en","",20595,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en","",20595,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en","",20595,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en","",20595,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en","",20595,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en","",20553,], ["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en",0,], ["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en",0,], -["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20595,], +["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20553,], ["libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en","",0,], ["libraries.matrix.ui.components_AvatarPickerSizes_Day_0_en","libraries.matrix.ui.components_AvatarPickerSizes_Night_0_en",0,], ["libraries.matrix.ui.components_AvatarPickerViewRtl_Day_0_en","libraries.matrix.ui.components_AvatarPickerViewRtl_Night_0_en",0,], @@ -146,22 +133,22 @@ export const screenshots = [ ["libraries.designsystem.modifiers_BackgroundVerticalGradientDisabled_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradientDisabled_Night_0_en",0,], ["libraries.designsystem.modifiers_BackgroundVerticalGradient_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradient_Night_0_en",0,], ["libraries.designsystem.components_Badge_Day_0_en","libraries.designsystem.components_Badge_Night_0_en",0,], -["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20595,], +["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20553,], ["libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en","libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en",0,], ["libraries.designsystem.components_BigIcon_Day_0_en","libraries.designsystem.components_BigIcon_Night_0_en",0,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20595,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20595,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20595,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20595,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20595,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20595,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20595,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20553,], ["libraries.designsystem.theme.components_BottomSheetDragHandle_Day_0_en","libraries.designsystem.theme.components_BottomSheetDragHandle_Night_0_en",0,], -["features.rageshake.impl.bugreport_BugReportViewDay_0_en","",20595,], -["features.rageshake.impl.bugreport_BugReportViewDay_1_en","",20595,], -["features.rageshake.impl.bugreport_BugReportViewDay_2_en","",20595,], -["features.rageshake.impl.bugreport_BugReportViewDay_3_en","",20595,], -["features.rageshake.impl.bugreport_BugReportViewDay_4_en","",20595,], +["features.rageshake.impl.bugreport_BugReportViewDay_0_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_1_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_2_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_3_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_4_en","",20553,], ["features.rageshake.impl.bugreport_BugReportViewNight_0_en","",0,], ["features.rageshake.impl.bugreport_BugReportViewNight_1_en","",0,], ["features.rageshake.impl.bugreport_BugReportViewNight_2_en","",0,], @@ -172,141 +159,141 @@ export const screenshots = [ ["features.messages.impl.timeline.components_CallMenuItem_Day_0_en","features.messages.impl.timeline.components_CallMenuItem_Night_0_en",0,], ["features.messages.impl.timeline.components_CallMenuItem_Day_1_en","features.messages.impl.timeline.components_CallMenuItem_Night_1_en",0,], ["features.messages.impl.timeline.components_CallMenuItem_Day_2_en","features.messages.impl.timeline.components_CallMenuItem_Night_2_en",0,], -["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20595,], -["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",20595,], +["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20553,], +["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",20553,], ["features.messages.impl.timeline.components_CallMenuItem_Day_5_en","features.messages.impl.timeline.components_CallMenuItem_Night_5_en",0,], -["features.messages.impl.timeline.components_CallMenuItem_Day_6_en","features.messages.impl.timeline.components_CallMenuItem_Night_6_en",20595,], +["features.messages.impl.timeline.components_CallMenuItem_Day_6_en","features.messages.impl.timeline.components_CallMenuItem_Night_6_en",20553,], ["features.messages.impl.timeline.components_CallMenuItem_Day_7_en","features.messages.impl.timeline.components_CallMenuItem_Night_7_en",0,], ["features.call.impl.ui_CallScreenView_Day_0_en","features.call.impl.ui_CallScreenView_Night_0_en",0,], -["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20595,], -["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20595,], -["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20595,], -["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20595,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20595,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_0_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_0_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en",20595,], +["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20553,], +["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20553,], +["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20553,], +["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20553,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_0_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_0_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en",20553,], ["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_5_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_5_en",0,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en",20595,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en",20595,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en",20595,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en",20595,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en",20595,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en",20595,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en",20595,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en",20595,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en",20595,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en",20553,], ["features.login.impl.changeserver_ChangeServerView_Day_0_en","features.login.impl.changeserver_ChangeServerView_Night_0_en",0,], -["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20595,], -["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20595,], -["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20595,], -["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20595,], -["features.login.impl.changeserver_ChangeServerView_Day_5_en","features.login.impl.changeserver_ChangeServerView_Night_5_en",20595,], +["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_5_en","features.login.impl.changeserver_ChangeServerView_Night_5_en",20553,], ["libraries.matrix.ui.components_CheckableResolvedUserRow_en","",0,], -["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20595,], +["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20553,], ["libraries.designsystem.theme.components_Checkboxes_Toggles_en","",0,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en",20595,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20595,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20595,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20595,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20595,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20595,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20595,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en",20595,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20553,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en",20553,], ["libraries.designsystem.theme.components_CircularProgressIndicator_Progress_Indicators_en","",0,], ["libraries.designsystem.components_ClickableLinkText_Text_en","",0,], -["features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_en","features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Night_0_en",20595,], ["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",20595,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20595,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_2_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_2_en",20595,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_3_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_3_en",20595,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_4_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_4_en",20595,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_5_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_5_en",20595,], -["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20595,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_2_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_2_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_3_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_3_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_4_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_4_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_5_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_5_en",20553,], +["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20553,], ["libraries.textcomposer_ComposerModeView_Day_1_en","libraries.textcomposer_ComposerModeView_Night_1_en",0,], ["libraries.textcomposer_ComposerModeView_Day_2_en","libraries.textcomposer_ComposerModeView_Night_2_en",0,], ["libraries.textcomposer_ComposerModeView_Day_3_en","libraries.textcomposer_ComposerModeView_Night_3_en",0,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en","",20595,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en","",20595,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20595,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20595,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20595,], -["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20595,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en","",20553,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20553,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20553,], +["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20553,], ["libraries.designsystem.components.dialogs_ConfirmationDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_ConfirmationDialog_Day_0_en","libraries.designsystem.components.dialogs_ConfirmationDialog_Night_0_en",0,], ["features.networkmonitor.api.ui_ConnectivityIndicator_Day_0_en","features.networkmonitor.api.ui_ConnectivityIndicator_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en","libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en",0,], -["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20595,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20595,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20595,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20595,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20595,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20595,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20595,], -["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20595,], -["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20595,], -["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20595,], -["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20595,], -["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20595,], -["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20595,], -["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20595,], -["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20595,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20595,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20595,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20595,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20595,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20595,], +["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20553,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20553,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20553,], +["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20553,], +["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20553,], +["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20553,], +["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20553,], +["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20553,], +["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20553,], +["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20553,], +["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20553,], ["libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_1_en",0,], -["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20595,], -["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20595,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20595,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20595,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20595,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20595,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20595,], +["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20553,], +["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20553,], ["features.logout.impl.direct_DefaultDirectLogoutView_Day_0_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_0_en",0,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20595,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20595,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20595,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20553,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20553,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20553,], ["features.logout.impl.direct_DefaultDirectLogoutView_Day_4_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_4_en",0,], -["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20595,], +["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20553,], ["features.licenses.impl.details_DependenciesDetailsView_Day_0_en","features.licenses.impl.details_DependenciesDetailsView_Night_0_en",0,], -["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20595,], -["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20595,], -["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20595,], -["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20595,], -["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_0_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_0_en",20595,], -["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_1_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_1_en",20595,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20595,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20595,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20595,], +["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20553,], +["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20553,], +["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20553,], +["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_0_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_1_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_1_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_3_en","features.preferences.impl.developer_DeveloperSettingsView_Night_3_en",20553,], ["libraries.designsystem.theme.components_DialogWithDestructiveButton_Dialog_with_destructive_button_Dialogs_en","",0,], ["libraries.designsystem.theme.components_DialogWithOnlyMessageAndOkButton_Dialog_with_only_message_and_ok_button_Dialogs_en","",0,], ["libraries.designsystem.theme.components_DialogWithThirdButton_Dialog_with_third_button_Dialogs_en","",0,], @@ -315,24 +302,25 @@ 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",20595,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20595,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20595,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20595,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20595,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_0_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_0_en",20595,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_1_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_1_en",20595,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_2_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_2_en",20595,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_3_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_3_en",20595,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_4_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_4_en",20595,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20595,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20595,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_2_en",20595,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en",20595,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_0_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_0_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_1_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_1_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_2_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_2_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_3_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_3_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_4_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_4_en",20553,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20553,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20553,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_2_en",20553,], ["libraries.matrix.ui.components_EditableOrgAvatarRtl_Day_0_en","libraries.matrix.ui.components_EditableOrgAvatarRtl_Night_0_en",0,], ["libraries.matrix.ui.components_EditableOrgAvatar_Day_0_en","libraries.matrix.ui.components_EditableOrgAvatar_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_Night_0_en",0,], @@ -340,29 +328,28 @@ export const screenshots = [ ["libraries.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Night_0_en",0,], ["features.messages.impl.timeline.components.customreaction_EmojiItem_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiItem_Night_0_en",0,], -["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en",20595,], -["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20595,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en",20553,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20553,], ["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en",0,], ["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en",0,], ["libraries.ui.common.nodes_EmptyView_Day_0_en","libraries.ui.common.nodes_EmptyView_Night_0_en",0,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en",20595,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en",20595,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en",20595,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en",20595,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en",20595,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en",20595,], -["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20595,], -["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20595,], -["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20595,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_0_en","features.linknewdevice.impl.screens.error_ErrorView_Night_0_en",20595,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_1_en","features.linknewdevice.impl.screens.error_ErrorView_Night_1_en",20595,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_2_en","features.linknewdevice.impl.screens.error_ErrorView_Night_2_en",20595,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_3_en","features.linknewdevice.impl.screens.error_ErrorView_Night_3_en",20595,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_4_en","features.linknewdevice.impl.screens.error_ErrorView_Night_4_en",20595,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_5_en","features.linknewdevice.impl.screens.error_ErrorView_Night_5_en",20595,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_6_en","features.linknewdevice.impl.screens.error_ErrorView_Night_6_en",20595,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_7_en","features.linknewdevice.impl.screens.error_ErrorView_Night_7_en",20595,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_8_en","features.linknewdevice.impl.screens.error_ErrorView_Night_8_en",20595,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en",20553,], +["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20553,], +["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_0_en","features.linknewdevice.impl.screens.error_ErrorView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_1_en","features.linknewdevice.impl.screens.error_ErrorView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_2_en","features.linknewdevice.impl.screens.error_ErrorView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_3_en","features.linknewdevice.impl.screens.error_ErrorView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_4_en","features.linknewdevice.impl.screens.error_ErrorView_Night_4_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_5_en","features.linknewdevice.impl.screens.error_ErrorView_Night_5_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_6_en","features.linknewdevice.impl.screens.error_ErrorView_Night_6_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_7_en","features.linknewdevice.impl.screens.error_ErrorView_Night_7_en",20553,], ["features.messages.impl.timeline.debug_EventDebugInfoView_Day_0_en","features.messages.impl.timeline.debug_EventDebugInfoView_Night_0_en",0,], ["libraries.designsystem.components_ExpandableBottomSheetLayout_en","",0,], ["libraries.featureflag.ui_FeatureListView_Day_0_en","libraries.featureflag.ui_FeatureListView_Night_0_en",0,], @@ -382,49 +369,47 @@ 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",20595,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20595,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20595,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20553,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20553,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20553,], ["features.messages.impl.timeline.components_FocusedEvent_Day_0_en","features.messages.impl.timeline.components_FocusedEvent_Night_0_en",0,], ["libraries.textcomposer.components_FormattingOption_Day_0_en","libraries.textcomposer.components_FormattingOption_Night_0_en",0,], ["features.forward.impl_ForwardMessagesView_Day_0_en","features.forward.impl_ForwardMessagesView_Night_0_en",0,], ["features.forward.impl_ForwardMessagesView_Day_1_en","features.forward.impl_ForwardMessagesView_Night_1_en",0,], ["features.forward.impl_ForwardMessagesView_Day_2_en","features.forward.impl_ForwardMessagesView_Night_2_en",0,], -["features.forward.impl_ForwardMessagesView_Day_3_en","features.forward.impl_ForwardMessagesView_Night_3_en",20595,], -["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20595,], -["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",20595,], +["features.forward.impl_ForwardMessagesView_Day_3_en","features.forward.impl_ForwardMessagesView_Night_3_en",20553,], +["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20553,], ["libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en","libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en",0,], ["libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en","libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en",0,], ["features.messages.impl.timeline.components.group_GroupHeaderView_Day_0_en","features.messages.impl.timeline.components.group_GroupHeaderView_Night_0_en",0,], ["libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en","libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en",0,], ["libraries.designsystem.atomic.pages_HeaderFooterPage_Day_0_en","libraries.designsystem.atomic.pages_HeaderFooterPage_Night_0_en",0,], -["features.home.impl.spaces_HomeSpacesView_Day_0_en","features.home.impl.spaces_HomeSpacesView_Night_0_en",20595,], -["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20595,], -["features.home.impl.spaces_HomeSpacesView_Day_2_en","features.home.impl.spaces_HomeSpacesView_Night_2_en",20595,], -["features.home.impl.components_HomeTopBarMultiAccount_Day_0_en","features.home.impl.components_HomeTopBarMultiAccount_Night_0_en",20595,], -["features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en","features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en",20595,], +["features.home.impl.spaces_HomeSpacesView_Day_0_en","features.home.impl.spaces_HomeSpacesView_Night_0_en",20553,], +["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20553,], +["features.home.impl.spaces_HomeSpacesView_Day_2_en","features.home.impl.spaces_HomeSpacesView_Night_2_en",20553,], +["features.home.impl.components_HomeTopBarMultiAccount_Day_0_en","features.home.impl.components_HomeTopBarMultiAccount_Night_0_en",20553,], +["features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en","features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en",20553,], ["features.home.impl.components_HomeTopBarSpaces_Day_0_en","features.home.impl.components_HomeTopBarSpaces_Night_0_en",0,], -["features.home.impl.components_HomeTopBarWithIndicator_Day_0_en","features.home.impl.components_HomeTopBarWithIndicator_Night_0_en",20595,], -["features.home.impl.components_HomeTopBar_Day_0_en","features.home.impl.components_HomeTopBar_Night_0_en",20595,], +["features.home.impl.components_HomeTopBarWithIndicator_Day_0_en","features.home.impl.components_HomeTopBarWithIndicator_Night_0_en",20553,], +["features.home.impl.components_HomeTopBar_Day_0_en","features.home.impl.components_HomeTopBar_Night_0_en",20553,], ["features.home.impl_HomeViewA11y_en","",0,], -["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20595,], -["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20595,], +["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20553,], +["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20553,], ["features.home.impl_HomeView_Day_11_en","features.home.impl_HomeView_Night_11_en",0,], ["features.home.impl_HomeView_Day_12_en","features.home.impl_HomeView_Night_12_en",0,], -["features.home.impl_HomeView_Day_13_en","features.home.impl_HomeView_Night_13_en",20595,], -["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20595,], -["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20595,], -["features.home.impl_HomeView_Day_16_en","features.home.impl_HomeView_Night_16_en",20595,], -["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20595,], -["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20595,], -["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20595,], -["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20595,], -["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20595,], -["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20595,], -["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20595,], -["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20595,], -["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20595,], +["features.home.impl_HomeView_Day_13_en","features.home.impl_HomeView_Night_13_en",20553,], +["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20553,], +["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20553,], +["features.home.impl_HomeView_Day_16_en","features.home.impl_HomeView_Night_16_en",20553,], +["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20553,], +["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20553,], +["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20553,], +["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20553,], +["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20553,], +["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20553,], +["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20553,], +["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20553,], +["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20553,], ["libraries.designsystem.theme.components_HorizontalDivider_Dividers_en","",0,], ["libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en","libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en",0,], ["libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en","libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en",0,], @@ -435,12 +420,12 @@ export const screenshots = [ ["libraries.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_Day_0_en","libraries.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_Night_0_en",0,], ["libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Day_0_en","libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Night_0_en",0,], ["libraries.designsystem.theme.components_IconToggleButton_Toggles_en","",0,], -["appicon.element_Icon_en","",0,], ["appicon.enterprise_Icon_en","",0,], +["appicon.element_Icon_en","",0,], ["libraries.designsystem.icons_IconsOther_Day_0_en","libraries.designsystem.icons_IconsOther_Night_0_en",0,], ["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_0_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_0_en",0,], -["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20595,], -["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20595,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20553,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20553,], ["libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_0_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_0_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_10_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_10_en",0,], @@ -448,119 +433,117 @@ export const screenshots = [ ["libraries.matrix.ui.messages.reply_InReplyToView_Day_1_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_1_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_2_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_2_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_3_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_3_en",0,], -["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20595,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20553,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_6_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_6_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_7_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_7_en",0,], -["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20595,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20553,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_9_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_9_en",0,], -["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20595,], -["features.call.impl.ui_IncomingCallScreen_Day_1_en","features.call.impl.ui_IncomingCallScreen_Night_1_en",20595,], +["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20553,], +["features.call.impl.ui_IncomingCallScreen_Day_1_en","features.call.impl.ui_IncomingCallScreen_Night_1_en",20553,], ["features.verifysession.impl.incoming_IncomingVerificationViewA11y_en","",0,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20595,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20595,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20553,], ["libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en","libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en",0,], ["libraries.designsystem.atomic.organisms_InfoListOrganism_Day_0_en","libraries.designsystem.atomic.organisms_InfoListOrganism_Night_0_en",0,], ["libraries.matrix.ui.media_InitialsAvatarBitmapGenerator_Day_0_en","libraries.matrix.ui.media_InitialsAvatarBitmapGenerator_Night_0_en",0,], -["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20595,], -["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20595,], -["features.invitepeople.impl_InvitePeopleView_Day_10_en","features.invitepeople.impl_InvitePeopleView_Night_10_en",20595,], -["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",20595,], +["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20553,], ["features.invitepeople.impl_InvitePeopleView_Day_2_en","features.invitepeople.impl_InvitePeopleView_Night_2_en",0,], ["features.invitepeople.impl_InvitePeopleView_Day_3_en","features.invitepeople.impl_InvitePeopleView_Night_3_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20595,], -["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20595,], -["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20595,], -["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20595,], +["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20553,], ["features.invitepeople.impl_InvitePeopleView_Day_8_en","features.invitepeople.impl_InvitePeopleView_Night_8_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_9_en","features.invitepeople.impl_InvitePeopleView_Night_9_en",20595,], -["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20595,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20595,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20595,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20595,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20595,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20595,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20595,], +["features.invitepeople.impl_InvitePeopleView_Day_9_en","features.invitepeople.impl_InvitePeopleView_Night_9_en",20553,], +["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20553,], ["features.joinroom.impl_JoinRoomView_Day_0_en","features.joinroom.impl_JoinRoomView_Night_0_en",0,], -["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20595,], -["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20595,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20595,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20595,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20595,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20595,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20595,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20595,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20595,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20595,], +["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20553,], ["libraries.designsystem.components_LabelledCheckbox_Toggles_en","",0,], -["features.preferences.impl.labs_LabsView_Day_0_en","features.preferences.impl.labs_LabsView_Night_0_en",20595,], -["features.preferences.impl.labs_LabsView_Day_1_en","features.preferences.impl.labs_LabsView_Night_1_en",20595,], +["features.preferences.impl.labs_LabsView_Day_0_en","features.preferences.impl.labs_LabsView_Night_0_en",20553,], +["features.preferences.impl.labs_LabsView_Day_1_en","features.preferences.impl.labs_LabsView_Night_1_en",20553,], ["features.leaveroom.impl_LeaveRoomView_Day_0_en","features.leaveroom.impl_LeaveRoomView_Night_0_en",0,], -["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20595,], -["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20595,], -["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20595,], -["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20595,], -["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20595,], -["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20595,], -["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_10_en","features.space.impl.leave_LeaveSpaceView_Night_10_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20595,], -["features.space.impl.leave_LeaveSpaceView_Day_9_en","features.space.impl.leave_LeaveSpaceView_Night_9_en",20595,], +["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_10_en","features.space.impl.leave_LeaveSpaceView_Night_10_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_9_en","features.space.impl.leave_LeaveSpaceView_Night_9_en",20553,], ["libraries.designsystem.background_LightGradientBackground_Day_0_en","libraries.designsystem.background_LightGradientBackground_Night_0_en",0,], ["libraries.designsystem.theme.components_LinearProgressIndicator_Progress_Indicators_en","",0,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en",20595,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en",20595,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_2_en",20595,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en",20595,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en",20595,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en",20595,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en",20553,], ["features.messages.impl.link_LinkView_Day_0_en","features.messages.impl.link_LinkView_Night_0_en",0,], -["features.messages.impl.link_LinkView_Day_1_en","features.messages.impl.link_LinkView_Night_1_en",20595,], +["features.messages.impl.link_LinkView_Day_1_en","features.messages.impl.link_LinkView_Night_1_en",20553,], ["libraries.designsystem.components.dialogs_ListDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_ListDialog_Day_0_en","libraries.designsystem.components.dialogs_ListDialog_Night_0_en",0,], ["libraries.designsystem.theme.components_ListItemPrimaryActionWithIcon_List_item_-_Primary_action_&_Icon_List_items_en","",0,], @@ -613,124 +596,89 @@ 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",20595,], ["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",20595,], +["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20553,], ["libraries.designsystem.components_LocationPin_Day_0_en","libraries.designsystem.components_LocationPin_Night_0_en",0,], ["features.location.impl.common.ui_LocationShareRow_Day_0_en","features.location.impl.common.ui_LocationShareRow_Night_0_en",0,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20595,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20595,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20595,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20553,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20553,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20553,], ["appnav.loggedin_LoggedInView_Day_0_en","appnav.loggedin_LoggedInView_Night_0_en",0,], -["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20595,], -["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20595,], -["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20595,], -["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20595,], -["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20595,], -["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20595,], -["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20595,], -["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20595,], -["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20595,], -["features.login.impl.login_LoginModeView_Day_6_en","features.login.impl.login_LoginModeView_Night_6_en",20595,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20595,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20595,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20595,], -["features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en","features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en",20595,], -["features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en","features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en",20595,], -["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20595,], -["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20595,], -["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20595,], -["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20595,], -["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20595,], -["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20595,], -["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20595,], -["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20595,], -["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20595,], -["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20595,], -["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20595,], -["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20595,], +["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20553,], +["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20553,], +["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20553,], +["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20553,], +["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20553,], +["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20553,], +["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20553,], +["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20553,], +["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20553,], +["features.login.impl.login_LoginModeView_Day_6_en","features.login.impl.login_LoginModeView_Night_6_en",20553,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20553,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20553,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20553,], +["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20553,], +["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20553,], +["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20553,], +["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20553,], +["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20553,], +["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20553,], +["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20553,], +["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20553,], +["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20553,], +["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20553,], +["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20553,], +["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20553,], ["libraries.designsystem.components.button_MainActionButton_Buttons_en","",0,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en",20595,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en",20595,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en",20595,], -["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20595,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en",20553,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en",20553,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en",20553,], +["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20553,], ["libraries.textcomposer.components.markdown_MarkdownTextInput_Day_0_en","libraries.textcomposer.components.markdown_MarkdownTextInput_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutralWrapping_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutralWrapping_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomPositive_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomPositive_Night_0_en",0,], +["libraries.matrix.ui.components_MatrixUserHeaderPlaceholder_Day_0_en","libraries.matrix.ui.components_MatrixUserHeaderPlaceholder_Night_0_en",0,], ["libraries.matrix.ui.components_MatrixUserHeader_Day_0_en","libraries.matrix.ui.components_MatrixUserHeader_Night_0_en",0,], ["libraries.matrix.ui.components_MatrixUserHeader_Day_1_en","libraries.matrix.ui.components_MatrixUserHeader_Night_1_en",0,], ["libraries.matrix.ui.components_MatrixUserRow_Day_0_en","libraries.matrix.ui.components_MatrixUserRow_Night_0_en",0,], ["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",20595,], -["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en",20595,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20595,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en",20595,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en",20595,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en",20595,], +["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20553,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20553,], ["libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en",0,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20595,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20595,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20553,], ["libraries.mediaviewer.impl.local.image_MediaImageView_Day_0_en","libraries.mediaviewer.impl.local.image_MediaImageView_Night_0_en",0,], ["libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_0_en","libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_0_en",0,], ["libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_1_en","libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_1_en",0,], ["libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_2_en","libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_2_en",0,], ["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","",20595,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en","",20595,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en","",20595,], -["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_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","",20595,], -["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_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","",20595,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20595,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20553,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20553,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_13_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20595,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20553,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_15_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_16_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_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_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","",20595,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20553,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_3_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_4_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_5_en","",0,], @@ -744,7 +692,7 @@ export const screenshots = [ ["libraries.textcomposer.mentions_MentionSpanTheme_Day_0_en","libraries.textcomposer.mentions_MentionSpanTheme_Night_0_en",0,], ["libraries.designsystem.theme.components.previews_Menu_Menus_en","",0,], ["features.messages.impl.messagecomposer_MessageComposerViewVoice_Day_0_en","features.messages.impl.messagecomposer_MessageComposerViewVoice_Night_0_en",0,], -["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20595,], +["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20553,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_0_en","features.messages.impl.timeline.components_MessageEventBubble_Night_0_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_1_en","features.messages.impl.timeline.components_MessageEventBubble_Night_1_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_2_en","features.messages.impl.timeline.components_MessageEventBubble_Night_2_en",0,], @@ -753,7 +701,7 @@ export const screenshots = [ ["features.messages.impl.timeline.components_MessageEventBubble_Day_5_en","features.messages.impl.timeline.components_MessageEventBubble_Night_5_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_6_en","features.messages.impl.timeline.components_MessageEventBubble_Night_6_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_7_en","features.messages.impl.timeline.components_MessageEventBubble_Night_7_en",0,], -["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20595,], +["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20553,], ["features.messages.impl.timeline.components_MessageStateEventContainer_Day_0_en","features.messages.impl.timeline.components_MessageStateEventContainer_Night_0_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButtonAdd_Day_0_en","features.messages.impl.timeline.components_MessagesReactionButtonAdd_Night_0_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButtonExtra_Day_0_en","features.messages.impl.timeline.components_MessagesReactionButtonExtra_Night_0_en",0,], @@ -762,145 +710,139 @@ 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",20595,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20595,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20595,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20595,], -["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20595,], -["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20595,], -["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",20595,], -["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20595,], -["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20595,], -["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20595,], -["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20595,], -["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20595,], -["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20595,], -["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20595,], -["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20595,], -["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20595,], +["features.messages.impl.topbars_MessagesViewTopBar_Day_0_en","features.messages.impl.topbars_MessagesViewTopBar_Night_0_en",20553,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20553,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20553,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20553,], +["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20553,], +["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20553,], +["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20553,], +["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20553,], +["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20553,], +["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20553,], +["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20553,], +["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20553,], +["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20553,], +["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20553,], +["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20553,], ["features.migration.impl_MigrationView_Day_0_en","features.migration.impl_MigrationView_Night_0_en",0,], -["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20595,], -["features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en","features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en",0,], +["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20553,], ["libraries.designsystem.theme.components_ModalBottomSheetDark_Bottom_Sheets_en","",0,], ["libraries.designsystem.theme.components_ModalBottomSheetLight_Bottom_Sheets_en","",0,], ["appicon.element_MonochromeIcon_en","",0,], +["features.preferences.impl.root_MultiAccountSection_Day_0_en","features.preferences.impl.root_MultiAccountSection_Night_0_en",0,], ["libraries.designsystem.components.dialogs_MultipleSelectionDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_MultipleSelectionDialog_Day_0_en","libraries.designsystem.components.dialogs_MultipleSelectionDialog_Night_0_en",0,], ["libraries.designsystem.components.list_MutipleSelectionListItemSelectedTrailingContent_Multiple_selection_List_item_-_selection_in_trailing_content_List_items_en","",0,], ["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",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20595,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20595,], -["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20595,], +["features.home.impl.components_NewNotificationSoundBanner_Day_0_en","features.home.impl.components_NewNotificationSoundBanner_Night_0_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20553,], +["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20553,], ["features.linknewdevice.impl.screens.number.component_NumberTextField_Day_0_en","features.linknewdevice.impl.screens.number.component_NumberTextField_Night_0_en",0,], ["libraries.designsystem.atomic.pages_OnBoardingPage_Day_0_en","libraries.designsystem.atomic.pages_OnBoardingPage_Night_0_en",0,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20595,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20595,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20595,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20595,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20595,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20595,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20595,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20595,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_8_en","features.login.impl.screens.onboarding_OnBoardingView_Night_8_en",20595,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20553,], ["libraries.designsystem.background_OnboardingBackground_Day_0_en","libraries.designsystem.background_OnboardingBackground_Night_0_en",0,], -["libraries.matrix.ui.components_OrganizationHeader_Day_0_en","libraries.matrix.ui.components_OrganizationHeader_Night_0_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20595,], +["libraries.matrix.ui.components_OrganizationHeader_Day_0_en","libraries.matrix.ui.components_OrganizationHeader_Night_0_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20553,], ["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_12_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_12_en",0,], ["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_13_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_13_en",0,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20595,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20595,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20553,], ["libraries.designsystem.theme.components_OutlinedButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonLarge_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMediumLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMedium_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonSmall_Buttons_en","",0,], -["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20595,], -["features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Day_0_en","features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Night_0_en",20595,], -["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20595,], -["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20595,], -["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20595,], -["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20595,], +["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20553,], +["features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Day_0_en","features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Night_0_en",20553,], +["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20553,], +["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20553,], +["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20553,], +["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20553,], ["features.lockscreen.impl.components_PinEntryTextField_Day_0_en","features.lockscreen.impl.components_PinEntryTextField_Night_0_en",0,], ["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",20595,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_8_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_9_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_8_en","features.lockscreen.impl.unlock_PinUnlockView_Night_8_en",20595,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_9_en","features.lockscreen.impl.unlock_PinUnlockView_Night_9_en",20595,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20553,], ["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_0_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_0_en",0,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20595,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20595,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20595,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20595,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20595,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20595,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20595,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20595,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20595,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20595,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20595,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20595,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20595,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20595,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20553,], ["libraries.designsystem.atomic.atoms_PlaceholderAtom_Day_0_en","libraries.designsystem.atomic.atoms_PlaceholderAtom_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_PlaybackSpeedButton_Day_0_en","libraries.designsystem.atomic.atoms_PlaybackSpeedButton_Night_0_en",0,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20595,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20595,], -["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20595,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20595,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20595,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20553,], ["features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en",0,], ["features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en",0,], -["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20595,], -["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20595,], -["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20595,], -["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20595,], -["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20595,], -["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20595,], -["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20595,], -["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20595,], -["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20595,], -["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20595,], -["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20595,], +["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20553,], ["features.poll.api.pollcontent_PollTitleView_Day_0_en","features.poll.api.pollcontent_PollTitleView_Night_0_en",0,], ["libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceCheckbox_Preferences_en","",0,], @@ -914,225 +856,215 @@ export const screenshots = [ ["libraries.designsystem.components.preferences_PreferenceRow_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceSlide_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceSwitch_Preferences_en","",0,], -["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewDark_2_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewDark_3_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewDark_4_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewDark_5_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewLight_2_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewLight_3_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewLight_4_en","",20595,], -["features.preferences.impl.root_PreferencesRootViewLight_5_en","",20595,], +["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20553,], +["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20553,], +["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20553,], +["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20553,], ["features.messages.impl.timeline.components.event_ProgressButton_Day_0_en","features.messages.impl.timeline.components.event_ProgressButton_Night_0_en",0,], -["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20595,], -["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20595,], +["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20553,], ["libraries.designsystem.components_ProgressDialogWithTextAndContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithTextAndContent_Night_0_en",0,], -["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20595,], -["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20595,], -["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20595,], -["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20595,], -["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20595,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20595,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20595,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20595,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20595,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20595,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20595,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20595,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20595,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20595,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20595,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20595,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20595,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20595,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20595,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20595,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20595,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20595,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20595,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20595,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20595,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20595,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20595,], +["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20553,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20553,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20553,], ["libraries.qrcode_QrCodeView_en","",0,], ["libraries.designsystem.theme.components_RadioButton_Toggles_en","",0,], -["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20595,], -["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20595,], +["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20553,], +["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20553,], ["features.rageshake.api.preferences_RageshakePreferencesView_Day_1_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_1_en",0,], ["features.messages.impl.timeline.components.reactionsummary_ReactionSummaryViewContent_Day_0_en","features.messages.impl.timeline.components.reactionsummary_ReactionSummaryViewContent_Night_0_en",0,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20595,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20595,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20595,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20595,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20595,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20595,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20595,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20553,], ["libraries.designsystem.atomic.atoms_RedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_RedIndicatorAtom_Night_0_en",0,], ["features.messages.impl.timeline.components_ReplySwipeIndicator_Day_0_en","features.messages.impl.timeline.components_ReplySwipeIndicator_Night_0_en",0,], -["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20595,], -["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20595,], -["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20595,], -["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20595,], -["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20595,], -["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20595,], -["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20595,], -["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20595,], -["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20595,], -["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20595,], -["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20595,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20595,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20595,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20595,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20595,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20595,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20595,], +["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20553,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20553,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20553,], ["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_0_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_0_en",0,], -["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20595,], -["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20595,], -["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20595,], -["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20595,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en",20595,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en",20595,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en",20595,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en",20595,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en",20595,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en",20595,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en",20595,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en",20595,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en",20595,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20553,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20553,], +["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en",20553,], ["libraries.matrix.ui.room.address_RoomAddressField_Day_0_en","libraries.matrix.ui.room.address_RoomAddressField_Night_0_en",0,], ["features.roomaliasresolver.impl_RoomAliasResolverView_Day_0_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_0_en",0,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",20595,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20595,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",20553,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20553,], ["features.roomdetails.impl_RoomDetailsA11y_en","",0,], -["features.roomdetails.impl_RoomDetailsDark_0_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_10_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_11_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_12_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_13_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_14_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_15_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_16_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_17_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_18_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_19_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_1_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_20_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_21_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_22_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_2_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_3_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_4_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_5_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_6_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_7_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_8_en","",20595,], -["features.roomdetails.impl_RoomDetailsDark_9_en","",20595,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_0_en",20595,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_1_en",20595,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_2_en",20595,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_3_en",20595,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_4_en",20595,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_5_en",20595,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_6_en",20595,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_7_en",20595,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_8_en",20595,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_9_en",20595,], -["features.roomdetails.impl_RoomDetails_0_en","",20595,], -["features.roomdetails.impl_RoomDetails_10_en","",20595,], -["features.roomdetails.impl_RoomDetails_11_en","",20595,], -["features.roomdetails.impl_RoomDetails_12_en","",20595,], -["features.roomdetails.impl_RoomDetails_13_en","",20595,], -["features.roomdetails.impl_RoomDetails_14_en","",20595,], -["features.roomdetails.impl_RoomDetails_15_en","",20595,], -["features.roomdetails.impl_RoomDetails_16_en","",20595,], -["features.roomdetails.impl_RoomDetails_17_en","",20595,], -["features.roomdetails.impl_RoomDetails_18_en","",20595,], -["features.roomdetails.impl_RoomDetails_19_en","",20595,], -["features.roomdetails.impl_RoomDetails_1_en","",20595,], -["features.roomdetails.impl_RoomDetails_20_en","",20595,], -["features.roomdetails.impl_RoomDetails_21_en","",20595,], -["features.roomdetails.impl_RoomDetails_22_en","",20595,], -["features.roomdetails.impl_RoomDetails_2_en","",20595,], -["features.roomdetails.impl_RoomDetails_3_en","",20595,], -["features.roomdetails.impl_RoomDetails_4_en","",20595,], -["features.roomdetails.impl_RoomDetails_5_en","",20595,], -["features.roomdetails.impl_RoomDetails_6_en","",20595,], -["features.roomdetails.impl_RoomDetails_7_en","",20595,], -["features.roomdetails.impl_RoomDetails_8_en","",20595,], -["features.roomdetails.impl_RoomDetails_9_en","",20595,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20595,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20595,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20595,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20595,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20595,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20595,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20595,], -["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20595,], -["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20595,], +["features.roomdetails.impl_RoomDetailsDark_0_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_10_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_11_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_12_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_13_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_14_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_15_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_16_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_17_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_18_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_19_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_1_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_20_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_21_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_22_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_2_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_3_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_4_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_5_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_6_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_7_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_8_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_9_en","",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_0_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_1_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_2_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_3_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_4_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_5_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_6_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_7_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_8_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_9_en",20553,], +["features.roomdetails.impl_RoomDetails_0_en","",20553,], +["features.roomdetails.impl_RoomDetails_10_en","",20553,], +["features.roomdetails.impl_RoomDetails_11_en","",20553,], +["features.roomdetails.impl_RoomDetails_12_en","",20553,], +["features.roomdetails.impl_RoomDetails_13_en","",20553,], +["features.roomdetails.impl_RoomDetails_14_en","",20553,], +["features.roomdetails.impl_RoomDetails_15_en","",20553,], +["features.roomdetails.impl_RoomDetails_16_en","",20553,], +["features.roomdetails.impl_RoomDetails_17_en","",20553,], +["features.roomdetails.impl_RoomDetails_18_en","",20553,], +["features.roomdetails.impl_RoomDetails_19_en","",20553,], +["features.roomdetails.impl_RoomDetails_1_en","",20553,], +["features.roomdetails.impl_RoomDetails_20_en","",20553,], +["features.roomdetails.impl_RoomDetails_21_en","",20553,], +["features.roomdetails.impl_RoomDetails_22_en","",20553,], +["features.roomdetails.impl_RoomDetails_2_en","",20553,], +["features.roomdetails.impl_RoomDetails_3_en","",20553,], +["features.roomdetails.impl_RoomDetails_4_en","",20553,], +["features.roomdetails.impl_RoomDetails_5_en","",20553,], +["features.roomdetails.impl_RoomDetails_6_en","",20553,], +["features.roomdetails.impl_RoomDetails_7_en","",20553,], +["features.roomdetails.impl_RoomDetails_8_en","",20553,], +["features.roomdetails.impl_RoomDetails_9_en","",20553,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20553,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20553,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20553,], +["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20553,], +["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20553,], ["features.home.impl.components_RoomListContentView_Day_2_en","features.home.impl.components_RoomListContentView_Night_2_en",0,], -["features.home.impl.components_RoomListContentView_Day_3_en","features.home.impl.components_RoomListContentView_Night_3_en",20595,], -["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20595,], -["features.home.impl.components_RoomListContentView_Day_5_en","features.home.impl.components_RoomListContentView_Night_5_en",20595,], -["features.home.impl.roomlist_RoomListContextMenu_Day_0_en","features.home.impl.roomlist_RoomListContextMenu_Night_0_en",20595,], -["features.home.impl.roomlist_RoomListContextMenu_Day_1_en","features.home.impl.roomlist_RoomListContextMenu_Night_1_en",20595,], -["features.home.impl.roomlist_RoomListContextMenu_Day_2_en","features.home.impl.roomlist_RoomListContextMenu_Night_2_en",20595,], -["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_0_en",20595,], -["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_1_en",20595,], -["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_2_en",20595,], -["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20595,], -["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20595,], +["features.home.impl.components_RoomListContentView_Day_3_en","features.home.impl.components_RoomListContentView_Night_3_en",20553,], +["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20553,], +["features.home.impl.components_RoomListContentView_Day_5_en","features.home.impl.components_RoomListContentView_Night_5_en",20553,], +["features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en",20553,], +["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20553,], +["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20553,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en",20553,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en",20553,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en",20553,], ["features.home.impl.search_RoomListSearchContent_Day_0_en","features.home.impl.search_RoomListSearchContent_Night_0_en",0,], -["features.home.impl.search_RoomListSearchContent_Day_1_en","features.home.impl.search_RoomListSearchContent_Night_1_en",20595,], -["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20595,], -["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20595,], -["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20595,], -["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20595,], -["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20595,], -["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20595,], -["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20595,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20595,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20595,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20595,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20595,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20595,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20595,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20595,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20595,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en",20595,], +["features.home.impl.search_RoomListSearchContent_Day_1_en","features.home.impl.search_RoomListSearchContent_Night_1_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en",20553,], ["features.roommembermoderation.impl_RoomMemberModerationView_Day_9_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_9_en",0,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20595,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20595,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20595,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20595,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20595,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20595,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20595,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20595,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20553,], ["libraries.designsystem.atomic.atoms_RoomPreviewAliasAtom_Day_0_en","libraries.designsystem.atomic.atoms_RoomPreviewAliasAtom_Night_0_en",0,], -["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20595,], -["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20595,], -["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20595,], -["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20595,], -["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20595,], -["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20595,], +["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20553,], ["features.home.impl.components_RoomSummaryPlaceholderRow_Day_0_en","features.home.impl.components_RoomSummaryPlaceholderRow_Night_0_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_0_en","features.home.impl.components_RoomSummaryRow_Night_0_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_10_en","features.home.impl.components_RoomSummaryRow_Night_10_en",0,], @@ -1155,17 +1087,16 @@ export const screenshots = [ ["features.home.impl.components_RoomSummaryRow_Day_26_en","features.home.impl.components_RoomSummaryRow_Night_26_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_27_en","features.home.impl.components_RoomSummaryRow_Night_27_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_28_en","features.home.impl.components_RoomSummaryRow_Night_28_en",0,], -["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20595,], -["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20595,], -["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20595,], -["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20595,], -["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20595,], -["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20595,], -["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20595,], -["features.home.impl.components_RoomSummaryRow_Day_35_en","features.home.impl.components_RoomSummaryRow_Night_35_en",20595,], +["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_35_en","features.home.impl.components_RoomSummaryRow_Night_35_en",20553,], ["features.home.impl.components_RoomSummaryRow_Day_36_en","features.home.impl.components_RoomSummaryRow_Night_36_en",0,], -["features.home.impl.components_RoomSummaryRow_Day_37_en","features.home.impl.components_RoomSummaryRow_Night_37_en",20595,], -["features.home.impl.components_RoomSummaryRow_Day_38_en","features.home.impl.components_RoomSummaryRow_Night_38_en",0,], +["features.home.impl.components_RoomSummaryRow_Day_37_en","features.home.impl.components_RoomSummaryRow_Night_37_en",20553,], ["features.home.impl.components_RoomSummaryRow_Day_3_en","features.home.impl.components_RoomSummaryRow_Night_3_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_4_en","features.home.impl.components_RoomSummaryRow_Night_4_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_5_en","features.home.impl.components_RoomSummaryRow_Night_5_en",0,], @@ -1173,119 +1104,118 @@ export const screenshots = [ ["features.home.impl.components_RoomSummaryRow_Day_7_en","features.home.impl.components_RoomSummaryRow_Night_7_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_8_en","features.home.impl.components_RoomSummaryRow_Night_8_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_9_en","features.home.impl.components_RoomSummaryRow_Night_9_en",0,], -["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_0_en","appnav.root_RootView_Night_0_en",20595,], -["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20595,], -["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20595,], +["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20553,], +["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20553,], +["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20553,], ["appicon.element_RoundIcon_en","",0,], ["appicon.enterprise_RoundIcon_en","",0,], ["libraries.designsystem.atomic.atoms_RoundedIconAtom_Day_0_en","libraries.designsystem.atomic.atoms_RoundedIconAtom_Night_0_en",0,], -["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20595,], -["libraries.designsystem.components.dialogs_SaveChangesDialog_Day_0_en","libraries.designsystem.components.dialogs_SaveChangesDialog_Night_0_en",20595,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_0_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_0_en",20595,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_1_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_1_en",20595,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_2_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_2_en",20595,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_3_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_3_en",20595,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20595,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20595,], +["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20553,], +["libraries.designsystem.components.dialogs_SaveChangesDialog_Day_0_en","libraries.designsystem.components.dialogs_SaveChangesDialog_Night_0_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_0_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_1_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_2_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_3_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_3_en",20553,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20553,], ["libraries.designsystem.theme.components_SearchBarActiveNoneQuery_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchBarActiveWithContent_Search_views_en","",0,], -["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20595,], +["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20553,], ["libraries.designsystem.theme.components_SearchBarActiveWithQueryNoBackButton_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchBarActiveWithQuery_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchBarInactive_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchFieldsDark_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchFieldsLight_Search_views_en","",0,], -["features.startchat.impl.components_SearchMultipleUsersResultItem_en","",20595,], -["features.startchat.impl.components_SearchSingleUserResultItem_en","",20595,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20595,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20595,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20595,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20595,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20595,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20595,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20595,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20595,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20595,], -["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20595,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_0_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_1_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_2_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_3_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_0_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_1_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_2_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_3_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en","",20595,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en","",20595,], -["features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_en","features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Night_0_en",20595,], +["features.startchat.impl.components_SearchMultipleUsersResultItem_en","",20553,], +["features.startchat.impl.components_SearchSingleUserResultItem_en","",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_0_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_1_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_2_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_3_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_0_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_1_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_2_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_3_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en","",20553,], +["features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_en","features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Night_0_en",20553,], ["libraries.designsystem.atomic.atoms_SelectedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_SelectedIndicatorAtom_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedRoomRtl_Day_0_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedRoomRtl_Day_1_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_1_en",0,], @@ -1308,38 +1238,33 @@ export const screenshots = [ ["libraries.matrix.ui.messages.sender_SenderName_Day_6_en","libraries.matrix.ui.messages.sender_SenderName_Night_6_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_7_en","libraries.matrix.ui.messages.sender_SenderName_Night_7_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_8_en","libraries.matrix.ui.messages.sender_SenderName_Night_8_en",0,], -["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20595,], -["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20595,], -["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20595,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20595,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20595,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20595,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20595,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20595,], -["features.location.impl.share_ShareLocationView_Day_0_en","features.location.impl.share_ShareLocationView_Night_0_en",20595,], -["features.location.impl.share_ShareLocationView_Day_1_en","features.location.impl.share_ShareLocationView_Night_1_en",20595,], -["features.location.impl.share_ShareLocationView_Day_2_en","features.location.impl.share_ShareLocationView_Night_2_en",20595,], -["features.location.impl.share_ShareLocationView_Day_3_en","features.location.impl.share_ShareLocationView_Night_3_en",20595,], -["features.location.impl.share_ShareLocationView_Day_4_en","features.location.impl.share_ShareLocationView_Night_4_en",20595,], -["features.location.impl.share_ShareLocationView_Day_5_en","features.location.impl.share_ShareLocationView_Night_5_en",20595,], -["features.location.impl.share_ShareLocationView_Day_6_en","features.location.impl.share_ShareLocationView_Night_6_en",20595,], -["features.location.impl.share_ShareLocationView_Day_7_en","features.location.impl.share_ShareLocationView_Night_7_en",20595,], -["features.location.impl.share_ShareLocationView_Day_8_en","features.location.impl.share_ShareLocationView_Night_8_en",20595,], +["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20553,], +["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20553,], +["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20553,], +["features.location.impl.share_ShareLocationView_Day_0_en","features.location.impl.share_ShareLocationView_Night_0_en",20553,], +["features.location.impl.share_ShareLocationView_Day_1_en","features.location.impl.share_ShareLocationView_Night_1_en",20553,], +["features.location.impl.share_ShareLocationView_Day_2_en","features.location.impl.share_ShareLocationView_Night_2_en",20553,], +["features.location.impl.share_ShareLocationView_Day_3_en","features.location.impl.share_ShareLocationView_Night_3_en",20553,], +["features.location.impl.share_ShareLocationView_Day_4_en","features.location.impl.share_ShareLocationView_Night_4_en",20553,], +["features.location.impl.share_ShareLocationView_Day_5_en","features.location.impl.share_ShareLocationView_Night_5_en",20553,], +["features.location.impl.share_ShareLocationView_Day_6_en","features.location.impl.share_ShareLocationView_Night_6_en",20553,], ["features.share.impl_ShareView_Day_0_en","features.share.impl_ShareView_Night_0_en",0,], ["features.share.impl_ShareView_Day_1_en","features.share.impl_ShareView_Night_1_en",0,], ["features.share.impl_ShareView_Day_2_en","features.share.impl_ShareView_Night_2_en",0,], -["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20595,], -["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20595,], -["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20595,], -["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20595,], -["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20595,], -["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20595,], -["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20595,], -["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20595,], -["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20595,], -["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en",20595,], -["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en",20598,], -["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20595,], +["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20553,], +["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20553,], +["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20553,], +["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20553,], +["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20553,], +["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20553,], +["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20553,], +["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en",20553,], +["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20553,], ["libraries.designsystem.components_SimpleModalBottomSheet_Day_0_en","libraries.designsystem.components_SimpleModalBottomSheet_Night_0_en",0,], ["libraries.designsystem.components.dialogs_SingleSelectionDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_SingleSelectionDialog_Day_0_en","libraries.designsystem.components.dialogs_SingleSelectionDialog_Night_0_en",0,], @@ -1349,106 +1274,107 @@ export const screenshots = [ ["libraries.designsystem.components.list_SingleSelectionListItemUnselectedWithSupportingText_Single_selection_List_item_-_no_selection,_supporting_text_List_items_en","",0,], ["libraries.designsystem.components.list_SingleSelectionListItem_Single_selection_List_item_-_no_selection_List_items_en","",0,], ["libraries.designsystem.theme.components_Sliders_Sliders_en","",0,], -["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20595,], +["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20553,], ["libraries.designsystem.theme.components_SnackbarWithActionAndCloseButton_Snackbar_with_action_and_close_button_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithActionOnNewLineAndCloseButton_Snackbar_with_action_and_close_button_on_new_line_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithActionOnNewLine_Snackbar_with_action_on_new_line_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithAction_Snackbar_with_action_Snackbars_en","",0,], ["libraries.designsystem.theme.components_Snackbar_Snackbar_Snackbars_en","",0,], +["features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en","features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en",20553,], ["libraries.designsystem.components.avatar.internal_SpaceAvatar_Avatars_en","",0,], -["features.home.impl.spacefilters_SpaceFiltersView_Day_0_en","features.home.impl.spacefilters_SpaceFiltersView_Night_0_en",20595,], -["features.home.impl.spacefilters_SpaceFiltersView_Day_1_en","features.home.impl.spacefilters_SpaceFiltersView_Night_1_en",20595,], -["libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20595,], -["libraries.matrix.ui.components_SpaceHeaderView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderView_Night_0_en",20595,], -["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20595,], +["features.home.impl.spacefilters_SpaceFiltersView_Day_0_en","features.home.impl.spacefilters_SpaceFiltersView_Night_0_en",20553,], +["features.home.impl.spacefilters_SpaceFiltersView_Day_1_en","features.home.impl.spacefilters_SpaceFiltersView_Night_1_en",20553,], +["libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20553,], +["libraries.matrix.ui.components_SpaceHeaderView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderView_Night_0_en",0,], +["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20553,], ["libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Day_0_en","libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Night_0_en",0,], ["libraries.matrix.ui.components_SpaceMembersView_Day_0_en","libraries.matrix.ui.components_SpaceMembersView_Night_0_en",0,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en",20595,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en",20595,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en",20595,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en",20595,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en",20595,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en",20595,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en",20595,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en",20595,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en",20595,], -["features.space.impl.settings_SpaceSettingsView_Day_0_en","features.space.impl.settings_SpaceSettingsView_Night_0_en",20595,], -["features.space.impl.settings_SpaceSettingsView_Day_1_en","features.space.impl.settings_SpaceSettingsView_Night_1_en",20595,], -["features.space.impl.settings_SpaceSettingsView_Day_2_en","features.space.impl.settings_SpaceSettingsView_Night_2_en",20595,], -["features.space.impl.settings_SpaceSettingsView_Day_3_en","features.space.impl.settings_SpaceSettingsView_Night_3_en",20595,], -["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",20595,], -["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20595,], -["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20595,], -["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20595,], -["features.space.impl.root_SpaceView_Day_4_en","features.space.impl.root_SpaceView_Night_4_en",20595,], -["features.space.impl.root_SpaceView_Day_5_en","features.space.impl.root_SpaceView_Night_5_en",20595,], -["features.space.impl.root_SpaceView_Day_6_en","features.space.impl.root_SpaceView_Night_6_en",20595,], -["features.space.impl.root_SpaceView_Day_7_en","features.space.impl.root_SpaceView_Night_7_en",20595,], -["features.space.impl.root_SpaceView_Day_8_en","features.space.impl.root_SpaceView_Night_8_en",20595,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_0_en","features.space.impl.settings_SpaceSettingsView_Night_0_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_1_en","features.space.impl.settings_SpaceSettingsView_Night_1_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_2_en","features.space.impl.settings_SpaceSettingsView_Night_2_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_3_en","features.space.impl.settings_SpaceSettingsView_Night_3_en",20553,], +["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",20553,], +["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20553,], +["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20553,], +["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20553,], +["features.space.impl.root_SpaceView_Day_4_en","features.space.impl.root_SpaceView_Night_4_en",20553,], +["features.space.impl.root_SpaceView_Day_5_en","features.space.impl.root_SpaceView_Night_5_en",20553,], +["features.space.impl.root_SpaceView_Day_6_en","features.space.impl.root_SpaceView_Night_6_en",20553,], +["features.space.impl.root_SpaceView_Day_7_en","features.space.impl.root_SpaceView_Night_7_en",20553,], +["features.space.impl.root_SpaceView_Day_8_en","features.space.impl.root_SpaceView_Night_8_en",20553,], ["libraries.designsystem.modifiers_SquareSizeModifierInsideSquare_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeHeight_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeWidth_en","",0,], -["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20595,], -["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20595,], -["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20595,], -["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20595,], -["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20595,], -["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20595,], +["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20553,], +["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20553,], +["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20553,], +["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20553,], +["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20553,], +["features.startchat.impl.root_StartChatView_Day_5_en","features.startchat.impl.root_StartChatView_Night_5_en",20553,], +["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20553,], ["features.location.api_StaticMapView_Day_0_en","features.location.api_StaticMapView_Night_0_en",0,], -["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20595,], +["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20553,], ["libraries.designsystem.atomic.pages_SunsetPage_Day_0_en","libraries.designsystem.atomic.pages_SunsetPage_Night_0_en",0,], ["libraries.designsystem.components.button_SuperButton_Day_0_en","libraries.designsystem.components.button_SuperButton_Night_0_en",0,], ["libraries.designsystem.theme.components_Surface_en","",0,], ["libraries.designsystem.theme.components_Switch_Toggles_en","",0,], -["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20595,], +["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20553,], ["libraries.designsystem.components.avatar.internal_TextAvatar_Avatars_en","",0,], ["libraries.designsystem.theme.components_TextButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonLarge_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonMediumLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonMedium_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonSmall_Buttons_en","",0,], -["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20595,], -["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20595,], -["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20595,], -["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20595,], -["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20595,], -["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20595,], -["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20595,], -["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20595,], -["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20595,], -["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20595,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20595,], -["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20595,], -["libraries.textcomposer_TextComposerScaledDensityWithReply_en","",20595,], -["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20595,], -["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20595,], -["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20595,], +["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20553,], +["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20553,], +["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20553,], +["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20553,], +["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20553,], +["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20553,], +["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20553,], +["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20553,], +["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20553,], +["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20553,], ["libraries.textcomposer_TextComposerVoice_Day_0_en","libraries.textcomposer_TextComposerVoice_Night_0_en",0,], ["libraries.designsystem.theme.components_TextDark_Text_en","",0,], -["libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en",20595,], -["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20595,], +["libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en",20553,], +["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20553,], ["libraries.designsystem.components.list_TextFieldListItemEmpty_Text_field_List_item_-_empty_List_items_en","",0,], ["libraries.designsystem.components.list_TextFieldListItemTextFieldValue_Text_field_List_item_-_textfieldvalue_List_items_en","",0,], ["libraries.designsystem.components.list_TextFieldListItem_Text_field_List_item_-_text_List_items_en","",0,], @@ -1460,18 +1386,16 @@ export const screenshots = [ ["libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_3_en","libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_3_en",0,], ["libraries.textcomposer.components_TextFormatting_Day_0_en","libraries.textcomposer.components_TextFormatting_Night_0_en",0,], ["libraries.designsystem.theme.components_TextLight_Text_en","",0,], -["features.messages.impl.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",20595,], -["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20595,], -["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","",20595,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20595,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20595,], +["features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en","features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en",20553,], +["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20553,], +["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20553,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20553,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20553,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_0_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_1_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_2_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20595,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20595,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20553,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20553,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_7_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_7_en",0,], @@ -1481,18 +1405,18 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en",0,], -["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20595,], +["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20553,], ["features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Night_0_en",0,], ["features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Day_1_en","features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Night_1_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20595,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowLongSenderName_en","",0,], @@ -1500,18 +1424,18 @@ export const screenshots = [ ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20595,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20595,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20595,], -["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20595,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20595,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20595,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20595,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en",0,], @@ -1520,44 +1444,42 @@ export const screenshots = [ ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20595,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20595,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en",20595,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20595,], +["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20553,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en",0,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20595,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20595,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20553,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemInformativeView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemInformativeView_Night_0_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20595,], +["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20553,], ["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_0_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_3_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_4_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20595,], -["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20595,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20553,], +["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Night_0_en",0,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20595,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20595,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemReactionsView_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsView_Night_0_en",0,], -["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20595,], +["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20553,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_0_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_0_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_1_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_1_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_2_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_2_en",0,], @@ -1566,8 +1488,8 @@ export const screenshots = [ ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_5_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_5_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_6_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_6_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_7_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_7_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20595,], -["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20595,], +["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20553,], +["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemStateEventRow_Day_0_en","features.messages.impl.timeline.components_TimelineItemStateEventRow_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemStateView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemStateView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en",0,], @@ -1582,8 +1504,8 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_4_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_5_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20595,], -["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20595,], +["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20553,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en",0,], @@ -1606,84 +1528,85 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemVoiceView_Day_9_en","features.messages.impl.timeline.components.event_TimelineItemVoiceView_Night_9_en",0,], ["features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en",0,], -["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20595,], -["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20595,], -["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",20595,], -["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20595,], -["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20595,], -["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20595,], -["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20595,], -["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20595,], -["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20595,], -["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20595,], -["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20595,], +["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",0,], +["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20553,], ["features.messages.impl.timeline_TimelineView_Day_2_en","features.messages.impl.timeline_TimelineView_Night_2_en",0,], ["features.messages.impl.timeline_TimelineView_Day_3_en","features.messages.impl.timeline_TimelineView_Night_3_en",0,], -["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20595,], +["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20553,], ["features.messages.impl.timeline_TimelineView_Day_5_en","features.messages.impl.timeline_TimelineView_Night_5_en",0,], -["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20595,], +["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20553,], ["features.messages.impl.timeline_TimelineView_Day_7_en","features.messages.impl.timeline_TimelineView_Night_7_en",0,], ["features.messages.impl.timeline_TimelineView_Day_8_en","features.messages.impl.timeline_TimelineView_Night_8_en",0,], ["features.messages.impl.timeline_TimelineView_Day_9_en","features.messages.impl.timeline_TimelineView_Night_9_en",0,], ["libraries.designsystem.components.avatar.internal_TombstonedRoomAvatar_Avatars_en","",0,], ["libraries.designsystem.theme.components_TopAppBarStr_App_Bars_en","",0,], ["libraries.designsystem.theme.components_TopAppBar_App_Bars_en","",0,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20595,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20595,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20595,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20595,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20595,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20595,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20595,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20595,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20553,], ["features.messages.impl.typing_TypingNotificationView_Day_0_en","features.messages.impl.typing_TypingNotificationView_Night_0_en",0,], -["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20595,], -["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20595,], -["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20595,], -["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20595,], -["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20595,], -["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20595,], +["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20553,], ["features.messages.impl.typing_TypingNotificationView_Day_7_en","features.messages.impl.typing_TypingNotificationView_Night_7_en",0,], ["features.messages.impl.typing_TypingNotificationView_Day_8_en","features.messages.impl.typing_TypingNotificationView_Night_8_en",0,], ["libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Night_0_en",0,], -["libraries.matrix.ui.components_UnresolvedUserRow_en","",20595,], +["libraries.matrix.ui.components_UnresolvedUserRow_en","",20553,], ["libraries.designsystem.components.avatar.internal_UserAvatarColors_Day_0_en","libraries.designsystem.components.avatar.internal_UserAvatarColors_Night_0_en",0,], -["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20595,], -["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20595,], -["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20595,], -["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20595,], +["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20553,], +["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20553,], +["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20553,], +["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20553,], ["features.startchat.impl.components_UserListView_Day_3_en","features.startchat.impl.components_UserListView_Night_3_en",0,], ["features.startchat.impl.components_UserListView_Day_4_en","features.startchat.impl.components_UserListView_Night_4_en",0,], ["features.startchat.impl.components_UserListView_Day_5_en","features.startchat.impl.components_UserListView_Night_5_en",0,], ["features.startchat.impl.components_UserListView_Day_6_en","features.startchat.impl.components_UserListView_Night_6_en",0,], -["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20595,], +["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20553,], ["features.startchat.impl.components_UserListView_Day_8_en","features.startchat.impl.components_UserListView_Night_8_en",0,], -["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20595,], +["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20553,], ["features.preferences.impl.user_UserPreferences_Day_0_en","features.preferences.impl.user_UserPreferences_Night_0_en",0,], ["features.preferences.impl.user_UserPreferences_Day_1_en","features.preferences.impl.user_UserPreferences_Night_1_en",0,], -["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20595,], -["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20595,], -["features.userprofile.shared_UserProfileMainActionsSection_Day_0_en","features.userprofile.shared_UserProfileMainActionsSection_Night_0_en",20595,], -["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20595,], -["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20595,], -["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20595,], -["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20595,], -["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20595,], -["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20595,], -["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20595,], -["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20595,], -["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20595,], -["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20595,], +["features.preferences.impl.user_UserPreferences_Day_2_en","features.preferences.impl.user_UserPreferences_Night_2_en",0,], +["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20553,], +["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20553,], +["features.userprofile.shared_UserProfileMainActionsSection_Day_0_en","features.userprofile.shared_UserProfileMainActionsSection_Night_0_en",20553,], +["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20553,], +["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20553,], +["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20553,], +["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20553,], +["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20553,], +["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20553,], +["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20553,], +["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20553,], +["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20553,], +["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20553,], ["features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en","features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en",0,], ["libraries.designsystem.ruler_VerticalRuler_Day_0_en","libraries.designsystem.ruler_VerticalRuler_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en",0,], -["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20595,], -["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20595,], +["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20553,], +["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20553,], ["features.viewfolder.impl.file_ViewFileView_Day_0_en","features.viewfolder.impl.file_ViewFileView_Night_0_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_1_en","features.viewfolder.impl.file_ViewFileView_Night_1_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_2_en","features.viewfolder.impl.file_ViewFileView_Night_2_en",0,], -["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20595,], +["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20553,], ["features.viewfolder.impl.file_ViewFileView_Day_4_en","features.viewfolder.impl.file_ViewFileView_Night_4_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_5_en","features.viewfolder.impl.file_ViewFileView_Night_5_en",0,], ["features.viewfolder.impl.folder_ViewFolderView_Day_0_en","features.viewfolder.impl.folder_ViewFolderView_Night_0_en",0,], diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsSdkSpan.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsSdkSpan.kt index 81add882a5..92f79da7f9 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsSdkSpan.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsSdkSpan.kt @@ -7,12 +7,9 @@ package io.element.android.services.analytics.api -import androidx.annotation.Discouraged - /** * Represents an analytics span in the Rust SDK. */ -@Discouraged("This component can cause crashes of the app when using debug builds of the Rust SDK.") interface AnalyticsSdkSpan { /** Enters the span and starts collecting metrics. */ fun enter() diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt index 846d5ce5b1..8c29f11197 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt @@ -8,7 +8,6 @@ package io.element.android.services.analytics.api -import androidx.annotation.Discouraged import io.element.android.services.analyticsproviders.api.AnalyticsProvider import io.element.android.services.analyticsproviders.api.AnalyticsTransaction import io.element.android.services.analyticsproviders.api.trackers.AnalyticsTracker @@ -75,7 +74,6 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker { fun removeLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction? /** Enter a span inside the Rust SDK tracing system. If a [parentTraceId] is provided, the SDK trace will be added as a child of that trace. */ - @Discouraged("This method can cause crashes of the app when using debug builds of the Rust SDK.") fun enterSdkSpan(name: String?, parentTraceId: String?): AnalyticsSdkSpan } @@ -118,7 +116,6 @@ fun AnalyticsService.finishLongRunningTransaction( } ?: false } -@Discouraged("This method can cause crashes of the app when using debug builds of the Rust SDK.") inline fun AnalyticsService.inBridgeSdkSpan(parentTraceId: String?, block: (AnalyticsSdkSpan) -> T): T { val span = enterSdkSpan(name = null, parentTraceId = parentTraceId) return try { diff --git a/services/analyticsproviders/sentry/build.gradle.kts b/services/analyticsproviders/sentry/build.gradle.kts index 3350df864b..02dde35ef4 100644 --- a/services/analyticsproviders/sentry/build.gradle.kts +++ b/services/analyticsproviders/sentry/build.gradle.kts @@ -50,8 +50,6 @@ 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/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 33c2ee133b..d6effec06f 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,8 +34,6 @@ interface AppForegroundStateService { */ val isSyncingNotificationEvent: StateFlow - val isSharingLiveLocation: StateFlow - /** * Start observing the foreground state. */ @@ -55,6 +53,4 @@ 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 9dd37af732..c9fa31caca 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,8 +20,6 @@ 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() { @@ -40,10 +38,6 @@ 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 9174b92684..a61733cc22 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,15 +16,12 @@ 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 } @@ -44,8 +41,4 @@ class FakeAppForegroundStateService( override fun updateHasRingingCall(hasRingingCall: Boolean) { this.hasRingingCall.value = hasRingingCall } - - override fun updateIsSharingLiveLocation(isSharingLiveLocation: Boolean) { - this.isSharingLiveLocation.value = isSharingLiveLocation - } } diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt index 4570e09921..9a8ff23eb5 100644 --- a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt +++ b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt @@ -34,30 +34,5 @@ interface StringProvider { * stripped of styled text information. */ fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String - - /** - * Returns a localized formatted string from the application's package's - * default string table, substituting the format arguments as defined in - * [java.util.Formatter] and [java.lang.String.format], based on the given quantity. - */ fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String - - /** - * Similar to [getQuantityString] but with separate resource ids for singular and plural values. - * Useful when we want to use different strings for singular and plural forms but not mentioning the actual quantity in the string. - * In this case, we cannot use getQuantityString, because some locales have more than two plural forms, and require the quantity to - * be part of the resulting strings. - * @param resIdForOne Resource id for the case when [quantity] is 1. - * @param resIdForOthers Resource id for the other cases ([quantity] is not 1). - * @param quantity The quantity to determine whether to use singular or plural form. Must be greater than or equal to 1. - * @param formatArgs The format arguments that will be used for substitution in the resulting string. Will be applied to either - * the singular or plural string depending on the quantity. - * @return The localized string corresponding to the given quantity. - */ - fun getSimpleQuantityString( - @StringRes resIdForOne: Int, - @StringRes resIdForOthers: Int, - quantity: Int, - vararg formatArgs: Any?, - ): String } diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt index b0e14c1db9..b095348c41 100644 --- a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt +++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt @@ -28,14 +28,4 @@ class AndroidStringProvider(private val resources: Resources) : StringProvider { override fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String { return resources.getQuantityString(resId, quantity, *formatArgs) } - - override fun getSimpleQuantityString( - resIdForOne: Int, - resIdForOthers: Int, - quantity: Int, - vararg formatArgs: Any?, - ): String { - val resId = if (quantity == 1) resIdForOne else resIdForOthers - return resources.getString(resId, *formatArgs) - } } diff --git a/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/strings/FakeStringProvider.kt b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/strings/FakeStringProvider.kt index 0412a8a03a..c2867dca3b 100644 --- a/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/strings/FakeStringProvider.kt +++ b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/strings/FakeStringProvider.kt @@ -28,14 +28,4 @@ class FakeStringProvider( lastResIdParam = resId return defaultResult + " ($quantity) " + formatArgs.joinToString() } - - override fun getSimpleQuantityString( - resIdForOne: Int, - resIdForOthers: Int, - quantity: Int, - vararg formatArgs: Any?, - ): String { - lastResIdParam = if (quantity == 1) resIdForOne else resIdForOthers - return defaultResult + " ($quantity) " + formatArgs.joinToString() - } } diff --git a/settings.gradle.kts b/settings.gradle.kts index db03a32f18..8c7b608465 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,8 +21,6 @@ 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/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt index e7f82292a4..f47621da0e 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt @@ -48,7 +48,6 @@ class KonsistLicenseTest { .files .filter { it.moduleName.startsWith("enterprise").not() && - it.moduleName != "libraries/rustls-tls" && it.nameWithExtension != "locales.kt" && it.name.startsWith("Template ").not() } @@ -79,7 +78,6 @@ class KonsistLicenseTest { .scopeFromProject() .files .filter { - it.moduleName.endsWith("rustls-tls").not() && it.nameWithExtension != "locales.kt" && it.nameWithExtension != "KonsistLicenseTest.kt" && it.name.startsWith("Template ").not() diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 433237cd13..ff05970ca4 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -30,8 +30,7 @@ class KonsistPreviewTest { .assertTrue { it.hasNameEndingWith("Preview") && it.hasNameEndingWith("LightPreview").not() && - it.hasNameEndingWith("DarkPreview").not() && - it.hasNameEndingWith("BlackPreview").not() + it.hasNameEndingWith("DarkPreview").not() } } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt index 93df350c44..75d91c4a88 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt @@ -24,9 +24,4 @@ class InstrumentationStringProvider : StringProvider { override fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any?): String { return resource.getQuantityString(resId, quantity, *formatArgs) } - - override fun getSimpleQuantityString(resIdForOne: Int, resIdForOthers: Int, quantity: Int, vararg formatArgs: Any?): String { - val resId = if (quantity == 1) resIdForOne else resIdForOthers - return resource.getString(resId, *formatArgs) - } } 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 d7ce9e2d28..12cfe44b44 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,17 +6,15 @@ * 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.AndroidComposeUiTest -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule import io.element.android.libraries.designsystem.utils.LocalUiTestMode import org.junit.Assert.assertFalse +import org.junit.rules.TestRule import kotlin.coroutines.CoroutineContext object RobolectricDispatcherCleaner { @@ -54,7 +52,7 @@ object RobolectricDispatcherCleaner { } } -fun AndroidComposeUiTest.setSafeContent( +fun AndroidComposeTestRule.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 232116b385..6502882d7d 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,17 +6,12 @@ * 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 import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasClickAction import androidx.compose.ui.test.hasContentDescription @@ -31,25 +26,17 @@ import org.junit.rules.TestRule val trueMatcher = SemanticsMatcher("true matcher") { true } -fun AndroidComposeUiTest.clickOn( +fun AndroidComposeTestRule.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. */ @@ -58,13 +45,6 @@ fun AndroidComposeTestRule.pressBack() { onNode(hasContentDescription(text)).performClick() } -/** - * Press the back key. - */ -fun AndroidComposeUiTest.pressBackKey() { - activity!!.onBackPressedDispatcher.onBackPressed() -} - /** * Press the back key. */ @@ -76,12 +56,7 @@ fun SemanticsNodeInteractionsProvider.pressTag(tag: String) { onNode(hasTestTag(tag)).performClick() } -fun AndroidComposeUiTest.assertNoNodeWithText(@StringRes res: Int) { - val text = activity!!.getString(res) +fun AndroidComposeTestRule.assertNoNodeWithText(@StringRes res: Int) { + val text = activity.getString(res) onNodeWithText(text).assertDoesNotExist() } - -fun AndroidComposeUiTest.assertNodeWithTextIsDisplayed(@StringRes res: Int) { - val text = activity!!.getString(res) - onNodeWithText(text).assertIsDisplayed() -} diff --git a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt index e9bd34d10e..a611fa83cf 100644 --- a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt +++ b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt @@ -22,7 +22,6 @@ import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import app.cash.paparazzi.RenderExtension import app.cash.paparazzi.TestName -import com.android.resources.Density.DEFAULT_DENSITY import com.android.resources.NightMode import com.android.resources.ScreenOrientation import io.element.android.compound.theme.ElementTheme @@ -43,13 +42,12 @@ object ScreenshotTest { Locale.setDefault(locale) paparazzi.fixScreenshotName(preview, localeStr) - paparazzi.snapshot { CompositionLocalProvider( LocalInspectionMode provides true, LocalDensity provides Density( density = LocalDensity.current.density, - fontScale = preview.previewInfo.fontScale, + fontScale = 1.0f, ), LocalConfiguration provides Configuration().apply { setLocales(LocaleList(locale)) @@ -123,22 +121,19 @@ object PaparazziPreviewRule { deviceConfig: DeviceConfig = ScreenshotTest.defaultDeviceConfig, renderExtensions: Set = setOf(), ): Paparazzi { - val densityScale = deviceConfig.density.dpiValue.toFloat() / DEFAULT_DENSITY - val customScreenWidth = preview.previewInfo.widthDp.takeIf { it >= 0 }?.let { it * densityScale }?.toInt() + val densityScale = deviceConfig.density.dpiValue / 160f val customScreenHeight = preview.previewInfo.heightDp.takeIf { it >= 0 }?.let { it * densityScale }?.toInt() val isLandscape = preview.previewInfo.device.contains("landscape") return Paparazzi( deviceConfig = deviceConfig.copy( - screenWidth = customScreenWidth ?: deviceConfig.screenWidth, - screenHeight = customScreenHeight ?: deviceConfig.screenHeight, nightMode = when (preview.previewInfo.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { true -> NightMode.NIGHT false -> NightMode.NOTNIGHT }, locale = locale, softButtons = false, + screenHeight = customScreenHeight ?: deviceConfig.screenHeight, orientation = if (isLandscape) ScreenOrientation.LANDSCAPE else ScreenOrientation.PORTRAIT, - fontScale = preview.previewInfo.fontScale, ), maxPercentDifference = 0.01, renderExtensions = renderExtensions, 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 559c023aa7..8ab5f8a44a 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:ba67fa9a541371f84cdc16dcc0c069e0cfda486c2b21dbb885cee7e0d9b6b005 -size 26924 +oid sha256:5d225730fee6fae0d93396a6c47dcd7abd86ba8902ac521c96e05fa36f619bbd +size 25840 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 788daaf8df..e2037ca626 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:00f8ec369fa64c6bbd97b37a5b53481438e74abdb5d366d32e3acae29460659c -size 29075 +oid sha256:f27f94a461f5b4bc5289ac41691e3736a73ddcbb9e1f2a1d0c9390a0ce5cb44b +size 27919 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 6ac6642b35..9df8fe1585 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:0a38ade8b9a5e3ce85fbe63ae76d2e856ac46c6cdc012a4952cb191fcbe8979f -size 25577 +oid sha256:708a157d8a7cb5da2b2ea52c12f9b10c172ec380dfcec09583adfe2ea4af3d74 +size 24610 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 5451e763d5..00e1659da8 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:1ae9d78458ea0b9d4b258be0620698abb699bcbc70275d98adfd992d8d50ec59 -size 27541 +oid sha256:260ea7d9f891afdea78a6d546138cbad7586fe2210058d3774d549c9918c2ccd +size 26529 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 b45afb67bf..b5e7419267 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:49731638f35e9c7583ece7122e3690728f3d4183a65e2e49cd270d02fb93ac70 -size 15950 +oid sha256:48fa7b1415694f0a7ebb3de458edee8792f5643f681cc25627560f2e3d8ba491 +size 16331 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 14fd900451..1c8ce2991d 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:11a77776662896532ef2999e3b90aa93b3459bf9b7a5f461c06ccb3743612aab -size 14738 +oid sha256:ebf559265b2cd4ebba7c5439b759365ed8d31532988bda5d8c76bb7c8d76dd68 +size 14892 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 deleted file mode 100644 index 6b4037b3eb..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6c755a7dcfc2c9f48d449e570a3f1af1cde299fd90ddd4478e9a4c315cf03128 -size 23810 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 deleted file mode 100644 index e64ae4c3cf..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:efa5408c3af0fcd39659b8d30b21ace43be46d2876815ad592f802a7887b5eb9 -size 23678 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 deleted file mode 100644 index 82b04fc045..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1fc512e71e168473e0e976be3e3e6cd24b455273257ad0adf970fc2641da43bb -size 25679 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 deleted file mode 100644 index f52d785548..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de71af50bba66a285b5ef44e1a4fe76ce6dc2e094c4c09ddf701deb9a5db898d -size 22025 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 deleted file mode 100644 index eef2177c3b..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:88b3583d55c6855027f13a152a6fc414f6b9e11d65e5b5403463b84fafe38c3f -size 21891 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 deleted file mode 100644 index 87d3f56720..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e64367832735139b68269a8c679b9c6f2b929b906124a0ee93735cbdd9202d6d -size 23826 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en.png new file mode 100644 index 0000000000..70e3f6ae06 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22e3dce5359ba6dd21bd1c17e4421b8341c51809843394c95a3d80db7a235309 +size 25774 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en.png new file mode 100644 index 0000000000..ddea584902 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:671a0a0ce5571cb460fba789b58ec1e38b63b26ba61c1daeb5cccb986eb424b4 +size 24768 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_en.png deleted file mode 100644 index 686013e622..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2a58c642983f209f2c4bb01ee0239c11edab299f48129800cbd41fe7a9032ad4 -size 26854 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_en.png deleted file mode 100644 index 278dfc4ae5..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:43837ff4703be9631370b56097bd36006bce98d23f17db629742c957ce945e5a -size 42390 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_en.png deleted file mode 100644 index 63a523b4ae..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8b3025b589185d8d276bd3a809d8d94700268be1aec951fd02ff37072cef4998 -size 27431 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_0_en.png deleted file mode 100644 index d6392b8c0f..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d7acb04225299d448edf35ad1f420ceafcabc7759a8f7d94ce11eca70781f79 -size 25339 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_1_en.png deleted file mode 100644 index bf799e5042..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:56b210d5d8dc06de139735ca34f2b5a4e5e7d0db395dc2dbe7b1c9d479d8858d -size 40405 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_2_en.png deleted file mode 100644 index 27a4cd57b0..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ff47c937641549f63948d114510644aad02723a55e752587a1037752a2b3972 -size 25966 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en.png new file mode 100644 index 0000000000..9b17990dbe --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a81e2f9d2c3834004b49b925025478b886c065a12dc850f1c42733e457630b97 +size 22442 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en.png new file mode 100644 index 0000000000..5b45fb30de --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3348885402696e4b8005740a452384223f60a64c524c32713cfbd9b3041e494 +size 22297 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en.png new file mode 100644 index 0000000000..1ec1abe309 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a54d3171418082d8bdf442bb34c6b149e2707963f88dd7ea7cbff401534fc0 +size 23856 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en.png new file mode 100644 index 0000000000..e5a42fb07f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ea756319eeb2d9ab6ad2425498a7a9902acada24c3ab359f880b5934f2511e5 +size 21384 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en.png new file mode 100644 index 0000000000..2388eb323d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c75bbb8425fa4b01f9d0d9398d93785f2a149f4abb820dadbfadf17bf4a6673e +size 21077 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en.png new file mode 100644 index 0000000000..356ee32526 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99defb3eb8530142a74c083d4f4ec2e592d465b3d2cf869e1710031ad6a805c9 +size 23136 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 a0ab0a2192..bc93c4c316 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:62fee8e8966ff4df0d951aff1d13e8578c9c5a55e89c4407dcc8077c9b1b72af -size 22249 +oid sha256:95096ef9f55d57ea4774f8eaa91d92a01fd44eafd4670442d3db132ff843ede0 +size 21476 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 cce585fe3f..d1c7598599 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:d3226560d55a9fde8175206c0ce21cb4cd90d900d357003496bf74b719281166 -size 26191 +oid sha256:b53f36378be4b4da2608210cb3134923c6f4bf565652c80a1b45cad8216d67da +size 25485 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 6a278db2b7..54a3c425c1 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:643f712799406118deb87af3fab34e8838878dc04e0935b4e54dafaaebd03479 -size 20895 +oid sha256:d549ca3000080a6d0c416d7a45d530da96c73184309048126be6304393bc74f9 +size 20082 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 2b09d75bc9..341bcca1a6 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:c2638b57f0f4ab1876429f096065d121122eaca0b9b06600281526dbf51d7791 -size 24843 +oid sha256:a61181eab30d30f56c5407bb42395a268ff80f7dd455ea04a468f92739e0a87f +size 24194 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..8c6b1bd7cc 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:d8ee76c2369a9671cbe370f367718fcda5bb08a89ed5116accc96928a64e9724 +size 55689 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..7983ef5e8a 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:21a24fade9819efdb9114ec0ba3db21ec87cf93e32d896e22117fcd4f23e07ce +size 53601 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 deleted file mode 100644 index 18b186ce98..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index d1698e913f..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 19a824127e..3832b8d087 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:d30e200c3e73775ed7081d3fbdc9f8843c5a5bf9e5f1b9ab3535cc6f0ea7f1f6 -size 35946 +oid sha256:fe92b35b0fe2e5481eeb497f2ada1a639cb1a66f937b4245798edffea75b1f5c +size 35932 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 292e8f0ead..4821d3ab76 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:90e602edde377f866071ec78f391bb7cd0d9b59bec79be91d6a42108f604a32d -size 35833 +oid sha256:f60e02e48d483fe8d6b2bdf46192ccc8911fad1e79c7ac1e0272824cc04d3d1c +size 35648 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 6c9c87f774..720026f561 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:ced04df939214952c513aa8b4ef4c7a02dfe3bb3bce81f9ec6ea1827f639bf3f -size 24471 +oid sha256:60333d74c8d940b32621defc5cdb3c3776b2980c137244dd34304365ee9ff447 +size 24158 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 429daba97a..0fcd59d198 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:018e6226239c439fc683eead0acb733d1e06eebad54b1a6c2b43d8cbd2e822cc -size 24324 +oid sha256:5bec4c7d06dc2cc287d2165f588c6764e5314287119960947d7e24dbc19ce901 +size 23971 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 deleted file mode 100644 index 14a6d77fe9..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 f1f3896ab8..368e908499 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:a78b0677d896de960ce8ce7c5818b1d52004c91917c73c460a605e5e1342554f -size 35129 +oid sha256:7f6bc4bb2233067ab6d07e596f8d5ca294501a47a21451a1115aa46e238b31a5 +size 34996 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 51d6a1bc13..c9d622683c 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:31b97291b16fd0872891ad2341d95d34d8c0608f92e7a5e39b5f27d40769b111 -size 34955 +oid sha256:766e65cfceb2e9ff3895ecd5c2be2b80d7b2833b30a5c67a0075ef823bb77cc0 +size 34816 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 3bd50c5655..ab0b09c575 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:43cf679fbb9a83f14209e61129afb8e7f18a60f9bc127bfcf4f5dff05c79598e -size 24033 +oid sha256:bd50a4cb9848602b08b853f4fa36314b981a858b98349d68decfcbd6f402d719 +size 23631 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 2a85b2cf7f..4194458de9 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:7ee6f9fbf05261efe52c247da3869c68c6e87f5132c52da72e56cf833ea6864d -size 23833 +oid sha256:3e2de9b362092dbd08deadac70591d4682304c69981fe27cbbeae4f26233d4aa +size 23444 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 deleted file mode 100644 index d36887d3e5..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 474a2b43a5..136812ccab 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:cc0409c3e6c60994c81d83c429bbfb7ebc4ee53ac4114d1913b089a8068ca51e -size 30046 +oid sha256:11a877e3e4810777b7ddb15fa11b8410976142ee4fb5e9d4103f4248acf121cf +size 29685 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 fab5d228a6..e04f62d6aa 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:3670e0f777491b7f6101c3c1c3cc006e4848a8f6e481b9740fb42ee8807ccf9e -size 30024 +oid sha256:471129545a85e702e8b59391f47630f128f80e819b042e91ee356684e4ae2438 +size 29680 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 764cb92eef..e5f8926345 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:79b7dd684dcec4a06271eed34794e7546ba441ad2cc9796641ba570871e45cd3 -size 30659 +oid sha256:a932c055ae0704cd62358c5442658c1cb059d7d3c0c56518de3e763c8eab3c52 +size 30355 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 9a795c080c..a1d475ecdd 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:29233a6bad671608f5e1a1d20b24b454cfd70f8a846e4d9572cc86b8c7da45a0 -size 30843 +oid sha256:7ca49fb335880670a3b71fdd1f2e2be50e9a5a6677d2e17e9534c9724d9d55b1 +size 30534 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 58ae956b13..116337e4be 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:5d30ae8d9d3b3820dc17d7aff3bd5c6794f5a267993a5dae4dc601278ccaaa5c -size 33999 +oid sha256:be4f85040190c658d5734000e602b4cb43cb0a5ab2e2cea0a993959702c4722f +size 33681 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 7ba887d858..bf1561d23e 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:bf5b6e85e57aef0d52253dc7563bbe0aae3f38e4a65e2fdc8e0460bccf26b006 -size 33722 +oid sha256:29d7aac2eaef7850cc9b0f5583d6c9a8aef230525d40011a9a7cb5401b681d03 +size 33409 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 a0404e5e72..99c888caba 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:e0c446ba7fc8e0c0c68732ba34058b41e04d8641dcdbc83255f9417706044c8c -size 29067 +oid sha256:5cb49a80d6ff771484743be2efc4900d053ccf84372614b98e9284d66d19056f +size 28895 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 fedf612386..4415136e95 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:da2778e384c9f749e888690660f496b4af93b7cff6f93c9fef346580a477e1cf -size 29029 +oid sha256:68845451924a562238f2c75dced10697cae75171d976e19c0de927eacfdc3210 +size 28853 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 6faf2d1cba..13df6a6877 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:62c79d08701dad73ab3f5f7744cd2683febeb34cb8bab986685e0cd8bd33c0f6 -size 29773 +oid sha256:5cc21d3fe55a5dba7b9e70d445877b07f0c88bee61fbd64f929a2321473ea8ad +size 29581 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 d4c10cf76f..5e08ee2145 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:fad2e4dbc822c79fbd8569dd8db7a5c08b335793aff6923129175bd4479cb218 -size 29744 +oid sha256:cf6312a9e19892c9db949474cb9aa3b7572cc132d3d5474278abb3c4d91b7ab1 +size 29505 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 ff882fdaea..e84f38285e 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:7a85f79b0167efdc0277d05a41712c39d10115482242bb84a140c6647c28d85d -size 32895 +oid sha256:f24e410e2715019c5fbba40a2f79750781e8db0f4b36c772cb7b37f0d3b0d26f +size 32697 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 1fbbba121f..16f85bba9f 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:865d4c7ff07834027411a76767aaaa8aa0febf529b910eaa51e5de9bd8a776fa -size 32641 +oid sha256:fd1e04c468c0daae10917f8e66a8e5a6ef8f72fa02f3ad4ca507b714dae7898d +size 32449 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 deleted file mode 100644 index d2ab2dcb1e..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index c4593656d1..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 fb88b5b2a8..d6faf7dd8f 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:0ec014c00a19fb280f4e2047d873f1698eedde51a765948bd2652079921588a7 -size 18264 +oid sha256:9b4a99e2ea65816e35ec8fb1cf40d8aafebb6fd5ae688911d7d009dda15cd0f9 +size 18123 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 9bf1f8609c..db46722b7b 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:ba8f1717d752a93cdda51a6ee5e8bac043a4572aadc4d1c0dba2bfe80e6c55db -size 24372 +oid sha256:cfbe2c73c732d671278ed411d76915d04d6e7ffce130c2a6dbe30ea43628e231 +size 24247 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 139ae28bda..c0703e447a 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:7bc87f28234d11c58f5b43a7f87ee4e5a5d631e5a41ba11756c057a2a5eea1ff -size 21745 +oid sha256:e9168cc32d47f15de0ce961d841d872181b8aadf5151d4b5fd4d305c87cdda75 +size 21597 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 a36301f230..457e334ac2 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:c97be1636f039afaab65c1b6858c439fcf47e6643681a886fd9c24ab6c7b0901 -size 23901 +oid sha256:96448542138570c8615e24dd3b26be55145570c64ac9d410bbc4dac36280c345 +size 23769 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 78e7e0d1bf..4621b6c95e 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:90ca0d9059538259cd19e7e75f6adc11f21b3a1cc77aec8d5efaf3d55abf8505 -size 34006 +oid sha256:672e617ab62c72127a4c57bed6da8ba4710cfc24ca455ea6edc765d7a424a0f3 +size 33766 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 e6153a4a2d..52e64e87c3 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:1cd2061366439af2139a1ace15557ecdd8aa91c0d2f1e0adfc76a7d63e17133c -size 17615 +oid sha256:451565c6881885b51de299c2d71e77f78a62e68f96a75bc859050e64b87ef79e +size 17372 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 03c7fd3b0c..5610b70d00 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:541b4cd380fee2f2a445fe25a1171492c865b10980dd79de70f42b0f3dc120c4 -size 23475 +oid sha256:def1de8d3a76255e72e0d65dbbf6364f77c2764e2ac99b32a0145174cf0f3a28 +size 23244 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 3cc9326747..3784bbcac7 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:b71e2ba137ec1ffc65c07fc836074f7fa0ef310fb81a15db905db819ea29dd0e -size 20980 +oid sha256:1bd9567f7a504590da66993a2da6771c1a4157bc37301510f6d91d62efed7c16 +size 20745 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 d6bb6bcc8d..e511df48c7 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:de97697d988d0cc25626c7e31f153a7dca6987215edbe2e42ec16367ea9b4b3c -size 22952 +oid sha256:d662774653bcec6246ac6529ec98bcff77b02e4a6a8ef42ed0a640fed3770dff +size 22698 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 59789476b1..d35cdd7397 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:9b3a92c98a93b1d9e7f127a13c664c7275e26030d59a0ebcd3641428d9a21683 -size 32303 +oid sha256:ee6a0cf330b30e4fdc7cb8b4268251850f2f112e1ad0ce4af1166341f04a0638 +size 32103 diff --git a/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Day_0_en.png index aee573a6fc..dc7182b71e 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:855f37c9ca2dc6ddc9a699e42a1a3c784c26911552174735f46f31ad2588e977 -size 295172 +oid sha256:0c2adbbce73e57c601821ea9b511913b3df0efbdd3d42643fabe0d39655e04e6 +size 438908 diff --git a/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Night_0_en.png index b8ff9c5b98..41f55afa15 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.api.internal_StaticMapPlaceholder_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e99b3d1c62e4907f55beefa2414ff6324c62133ab723c13b68c906331e8ee072 -size 118086 +oid sha256:3baf32fc12535ffe246264dfe1f20db1fddddb970f86953e9877251dc3f74d3d +size 173356 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 deleted file mode 100644 index c431ee02e9..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.api_LiveLocationSharingBanner_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index e3a3776919..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.api_LiveLocationSharingBanner_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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_StaticMapView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.api_StaticMapView_Day_0_en.png index cedf0b1b7c..1735586b26 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:359960ea4b7be5ff9d766565007291f8585223052483736e17d4532c5f8af0c6 +size 252728 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..b18c069686 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:4b775500b1fdc6294d94dcb1f07f74c515c3eae31bc55d0faacfd86ff7bd1da3 +size 105526 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 afd74655d9..e48b3cd8ab 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:c9ebf3725fa875994cf1a15b30e2d4d533c2a9281f0f7d5d9f83f8685ba384d1 -size 18637 +oid sha256:2550638ee12b4181cea31caff0b5838a9cdb3a180c01d1188bc7c2726051b863 +size 16578 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 30fad74bc6..0f17f6d6a1 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:65c321199578618012d27afe478c0eaf6a67c44101e7d1d1c51a4c6c1fa9b93a -size 17897 +oid sha256:c880e4d01495868b3f0689d20d3cbf2050d6261be936421343bc1ac210aabeec +size 15959 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 0c9a67252c..cce7a48382 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:9f3fb75974ce37fc3ce5e303ab5573a0d9a769f3c079de5b0f6462b40e53c1cd -size 26713 +oid sha256:a97492422d54a6d6666c1ade693dc9b63bc9ca07c17d6c1f787c081984c09f68 +size 42470 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 deleted file mode 100644 index e8ae396119..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index cce7a48382..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_6_en.png index 06ec0c10e9..541b2a97e1 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:4031ae26bb8465a61021c15b6166a2186f8f99d7a4dbad7bb28aaf83493cbe69 -size 25907 +oid sha256:038e12f3caeef6ac8d5389b7cdc68138e089dcac335d0a5904adc55c9bcb7b1c +size 40642 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 deleted file mode 100644 index 7761c87d2e..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c9ed7610fbdabb3e88c35e2832d06566431d472ad95890c2fdcbb6d1b43d8feb -size 36751 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 deleted file mode 100644 index 541b2a97e1..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:038e12f3caeef6ac8d5389b7cdc68138e089dcac335d0a5904adc55c9bcb7b1c -size 40642 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_1_en.png index 65720f93f6..ff0295d9ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba0285628cb8f18c5d666e6727b213d3c7674d1782a7364c7b72ff906ad57eff -size 19684 +oid sha256:37ccae030071cc4801538dc5c753a6148ce7e465442edcc89877353b7f5675cb +size 37572 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_2_en.png index 26571d8a30..6f440d71d6 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed64e57bea072d5bdf232133c365db043f117e35ef180aa12c9b05bff40a1a92 -size 16437 +oid sha256:5f57485d56fd4d02731f797762d931c4d738c8693539da612e33403693cd4b08 +size 35976 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_3_en.png index ff0295d9ac..964ad077b5 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37ccae030071cc4801538dc5c753a6148ce7e465442edcc89877353b7f5675cb -size 37572 +oid sha256:c882f3f9ed18a64ecfa253284bf1dbdad5d38f524258b6463521d5185c1c32a7 +size 31530 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_4_en.png index 6f440d71d6..46226555db 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f57485d56fd4d02731f797762d931c4d738c8693539da612e33403693cd4b08 -size 35976 +oid sha256:cdf7b194a075902ab9434e272865293535ef39370fbb9cb172b3cf8774850c73 +size 19104 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_5_en.png index 964ad077b5..ceb1513af6 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c882f3f9ed18a64ecfa253284bf1dbdad5d38f524258b6463521d5185c1c32a7 -size 31530 +oid sha256:8988c700db517eef71d7f42c8e21ac819f51c92fb88c0c25cb400be7a5326c22 +size 19228 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 deleted file mode 100644 index 46226555db..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_6_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cdf7b194a075902ab9434e272865293535ef39370fbb9cb172b3cf8774850c73 -size 19104 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 deleted file mode 100644 index ceb1513af6..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8988c700db517eef71d7f42c8e21ac819f51c92fb88c0c25cb400be7a5326c22 -size 19228 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_1_en.png index 4e96ed69ce..6c424a1ffe 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4356f7de986f803b890d3bc95afdefd01e9eb42d775836c647a6d9fafe3bcc4a -size 19189 +oid sha256:6a62d7b4716f97f73dddd22fc3ecad30ef159da186ff2f2029772f4574a4f474 +size 36084 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_2_en.png index cfa358b892..72196c0b11 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ce9e4f5911a6dfd35655f362d122f95690a651d7c05e5ef6c4ea0621be2e628 -size 15783 +oid sha256:10d95b146c51895a0e9e816cf56aa216dfbf77e74ae3da10f9c3fa94468ba9ed +size 34500 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_3_en.png index 6c424a1ffe..da90a76ab1 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a62d7b4716f97f73dddd22fc3ecad30ef159da186ff2f2029772f4574a4f474 -size 36084 +oid sha256:64236eda401891b7a04c9240ed2b9b077b8c08b182b76f454cf0a4376daa740a +size 30345 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_4_en.png index 72196c0b11..eed60f472d 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10d95b146c51895a0e9e816cf56aa216dfbf77e74ae3da10f9c3fa94468ba9ed -size 34500 +oid sha256:da601c01dd487f9c66f78ada91954398e1dcdf699f1ba4f6d8f7661f7b8cc4b7 +size 18715 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_5_en.png index da90a76ab1..d3ee3b9e22 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64236eda401891b7a04c9240ed2b9b077b8c08b182b76f454cf0a4376daa740a -size 30345 +oid sha256:ca86fd5eea8c05a52fee801e1ca61c2e4e205ad9874e06d69b1d1f674585f87b +size 18842 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 deleted file mode 100644 index eed60f472d..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_6_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:da601c01dd487f9c66f78ada91954398e1dcdf699f1ba4f6d8f7661f7b8cc4b7 -size 18715 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 deleted file mode 100644 index d3ee3b9e22..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ca86fd5eea8c05a52fee801e1ca61c2e4e205ad9874e06d69b1d1f674585f87b -size 18842 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 deleted file mode 100644 index 5669a59d24..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 265044a0b2..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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_8_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_8_en.png deleted file mode 100644 index ff95140047..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 4bc9f6653b..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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_8_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_8_en.png deleted file mode 100644 index 8b7fa8b77c..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:80246e3957a0e3f98bce55ddd6d8fe09b5f140eee3837d80ca20253f6bcfb876 -size 37738 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 deleted file mode 100644 index 266dae3c81..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:60dd90eb571f473b78ecc629573bcf2faca02819022bdf57f05cc8b0876f4ab8 -size 31440 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 deleted file mode 100644 index 8a8d15acf9..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d60a8caa2441ff0618636d307d32d0944bdb77fb3b579c0959dc951b0318ad72 -size 35429 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 deleted file mode 100644 index fe27573765..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bfe8b59b8d5d7a1c33285072dec437060faae55bfa2a42687bd8895dd90409a6 -size 30310 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 c4a683e701..16668cae13 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:44e078c64ab932a6e9ce2d429efb5f44556d4a923f4949e75dd2bd99591c4533 -size 25242 +oid sha256:3d62d11d8d8ccbbbbe811f157ecc20f31096a017f4acb36061a29e2aaefbd3e8 +size 25132 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 2a6cbdf049..20a22487c4 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:a2d413d264af067d4e03743cd50be879c91796a3f0907f40cefcdc6b0e466897 -size 24020 +oid sha256:a6d0f6746f6ccea9db09a6d351e5aae7d10d4d623edf00059bfece647731c2e5 +size 23959 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 c4a683e701..16668cae13 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:44e078c64ab932a6e9ce2d429efb5f44556d4a923f4949e75dd2bd99591c4533 -size 25242 +oid sha256:3d62d11d8d8ccbbbbe811f157ecc20f31096a017f4acb36061a29e2aaefbd3e8 +size 25132 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 2a6cbdf049..20a22487c4 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:a2d413d264af067d4e03743cd50be879c91796a3f0907f40cefcdc6b0e466897 -size 24020 +oid sha256:a6d0f6746f6ccea9db09a6d351e5aae7d10d4d623edf00059bfece647731c2e5 +size 23959 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 3314252e26..4a5a13a36a 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:6e97cab70b9ee3e870154ad4ae4cf1bf0bb413facf8253f5f9fe9429eddf76b1 -size 60303 +oid sha256:0ef35fd3f5346870f11120a37c9db969453b7594bf9a0ccc71fe43e7fdade488 +size 62532 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png index d58e079ced..1b69f8f6a1 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:caaa2a0f4de455704a6b5c78e19c2f723be7deaeec470c77ff1e87df194dd95c -size 58620 +oid sha256:1501c2591f7df68404285770b1dad67360dddce074d4ce1c71223ea0baa0d1e4 +size 60873 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 048c9beb26..878e97a06e 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:647163b31578572c9bf4a63a1d110f732fc3b0c41c3991651996608d6265c536 -size 31960 +oid sha256:fbcf2216fe8f2653d44df345c157eb66f54ff8767c210bfeab5719d6d70230a8 +size 31686 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 d27a10bf39..05edb58b0f 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:6e447151f2a7aaf9052130d5b915e9aea8180880c66fa51afb06d212079a0ced -size 31159 +oid sha256:324b1392f40bb408b6469e656e774f5b1f7cbd9bb6557ec7a2587a971b548ccc +size 31288 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 4b7e849116..096b521d75 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:e215ffdf8ba90e3f7a047d25206dfef4a08d25137ee2e4d3e5c16f254282927c -size 33371 +oid sha256:1075e98dac6a50a282a39dfd5855833080becd377e1bf3f9db8fb20104bda80d +size 33479 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 276d9919f7..e738c998a3 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:76613fd85c3211bd2c96e50f2d481d2d994aad20368713c1da309d0147ce1101 -size 31015 +oid sha256:4f55c2e968f464ba9593dc8841a0e633c2f0a75cb3f85857aa3b4b716770629e +size 30831 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 c1da84be8e..ae7ffe2dd4 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:5f32133324f20fc97c3473ca6ea3b1705d3b0beab5bed11af9199e4f5b9ab124 -size 30410 +oid sha256:e8484c5300a8da5b120a24513b29bdd697dd3edd7d6b7c91e5925aa55c6290fd +size 30366 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 e06e5ccaaa..1cf5572c4e 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:00556b6c310f63b548bc1a88b3b882a93412a2a7dacff008862faeb2ad572eb3 -size 32671 +oid sha256:8b87fe16a96a8c34b49aab481f65c3223fd098360048b7dcd1ee8bef85fdf378 +size 32650 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 d13b526c94..98039ad35b 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:c95c7e7f68133f25bca9c33246983cdd1b448ca822b1221eba54c171c8f2e051 -size 35898 +oid sha256:647b5d2512180674b14a93903de4ee0539720dc1ddc7eaadec23940aa8b3befa +size 35934 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 aa8c5c7858..9751c25cb4 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:b4b6b193a6c9bd9188cde36056a512ebe10f9b24d07a33926b51876712b453b2 -size 24468 +oid sha256:010a03fc52fb0ec9b39701d75ef26c7f23ba6195e7d3e0c4e2b7e03518425b57 +size 24287 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 429daba97a..0fcd59d198 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:018e6226239c439fc683eead0acb733d1e06eebad54b1a6c2b43d8cbd2e822cc -size 24324 +oid sha256:5bec4c7d06dc2cc287d2165f588c6764e5314287119960947d7e24dbc19ce901 +size 23971 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 0fc27b0af4..ce263c7d02 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:ecc5f3820395a67eee396556344663844fb97dc843cda96b2545c954d133795f -size 35118 +oid sha256:fb22d79ff43ce9a1931537880829cf5d2c13aafe3c06136c80703f2599d676cf +size 35000 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 fb5e9a7448..2c4de61a77 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:dc0360e2705f3e029c1f8cabf91787a98f7d7b67e6d1f003c54ca4ffd52d08be -size 23961 +oid sha256:f3c16c9a89228f83eaaa7f9bdd7c0e36a333efb541738bd38411f9f853e20103 +size 23797 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 2a85b2cf7f..4194458de9 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:7ee6f9fbf05261efe52c247da3869c68c6e87f5132c52da72e56cf833ea6864d -size 23833 +oid sha256:3e2de9b362092dbd08deadac70591d4682304c69981fe27cbbeae4f26233d4aa +size 23444 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 afd2b2cbe5..a1b5d1bb59 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:94014619417d8c530dc80316c9aca36dc08997f516890d7199b24afa46e71c86 -size 50004 +oid sha256:919595acfee379e3aaee66a3abba61515e8bee35a9fe0250a8a227f772109864 +size 49693 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 f86302a8ee..665a36ad0a 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:0a3d3c574589f06729b58775ca37bab9711c5566adff366d4a0196eaa7e10ab7 -size 47410 +oid sha256:079ef0792efb31fbd773beb77a95bfa713213f8f5428ea1c12060094deac5442 +size 48620 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 3ee9db165b..32d07be159 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:277665230a31e1a47489a5124efa6dcaf51a5a442f0e6d4dfb510ff4896c9bd0 -size 48738 +oid sha256:17cd489adfd7a417a6972f8e36a61ecb2bcaf2a045d33e4d2236004271d4d9ff +size 48317 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 ba4e6885b7..9ad4c83991 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:2044881fe3e54179333fc0b125972b2b410a4d3657faa10769b964e8ae0d2643 -size 45278 +oid sha256:215e1dbc213d493ee6e24712aa93c9a103f1148a3ea2858ff185dfdc7084ea3b +size 46546 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_0_en.png index e35ea91583..05df1938e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:891555a0886151040ebc4b9e3e4d25c3b2dbcd5af000a10236e292dd2f43c09c -size 75570 +oid sha256:e186c2798642148ef8e1f02a23600a594378ca094f6e11adc46c74ef421e8a8b +size 77187 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_1_en.png index 81e769c771..b59547825f 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ef0f01ee15b681536a4f6f29cf6a7d1c5907f9e072cf3aa9bba122848a7507a -size 74085 +oid sha256:4ca225207ee507d079b91b41a2335ad021eba3dc5d84744fd6c131cfd4b0951c +size 75708 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_2_en.png index 35e15394e5..ddcbf9d296 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d6dfb5695cfd24ae8207d66d5fee7695c1b12e1e9310a2ce92f07447f567225 -size 57192 +oid sha256:4b70a67dc7317be9e3c14d05d5c83efb09bbef086e8e77ef9d8e243f8cf3e359 +size 61058 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_3_en.png index 2fc9e99769..05da4b6f40 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b551591af19b7ff970159df80ac752961d63fbd468217cf935ec39e6dc4f5bb -size 54146 +oid sha256:4fd997ceab36234f4c66871bdf8809caaef106b2108b24718441e8af61a70e8c +size 55414 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_4_en.png index ba112abfdc..20d502fe90 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd5b298306dc0d435067bb5f9b4e49a8f9d467e108d8715250540b80aea17c08 -size 51156 +oid sha256:a0a2a4ab19d5f32e5945d55d7eae9d8db198e7f5951051d35ee703e60f63b679 +size 52432 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_0_en.png index 35cb8afc49..7b87a30db3 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:546b6f6a18dd79a8baac9907bc8c3a19727b6bbf28db3f379873a56f7c960d8a -size 73549 +oid sha256:a4ce88cb56905fcdcdb511fe54edd4f0f8bb579e6de9a7bfcce5629937b192d0 +size 75027 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_1_en.png index e2a186c3aa..1c8ac57c29 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19b2715dfeeee653d97a98f7b12741ae289fd7864e4ccb1fe17e9c02dc3fc826 -size 71941 +oid sha256:45b95831771832d69d8e5b8dbb1224fa66f3790f6d8beb546f57087db0600906 +size 73382 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_2_en.png index 332b169553..fd3f6b8b68 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccaa46d8df1a2ab720a50a081cdfe159e9bb28a37c66ffd48340ac45f241a3af -size 54033 +oid sha256:87154a19bb45a01d125c6bf4a1e6a9f0a35842989cdaca07387956fa372b9357 +size 57952 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_3_en.png index 617ed1b8d2..3083916d7b 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e72138fef757a0b4877b8caf199163dc091fb2539a7aab9dadc9e1cd9e762d0 -size 51684 +oid sha256:f1c45208ce974f2c8bf8fdb45653b2a1e604d646d1d5f4ae302b3e8739dbfa59 +size 52893 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_4_en.png index 145d76ae43..29f5b5d345 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_AccountDeactivationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fc8e670655f4981a9a01d8ea60391eb5362c130717cf1c88044d8833e6ad06c -size 48249 +oid sha256:83822e71afc34ccfe3c9c2aec30f480cfc01eb51e92db21216d457be00de24c0 +size 49460 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 da7923cf50..d996699f1a 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:b1c09cbb0217a1fb844828590e7827c27a2e9a99ecf5aed225f4d1bd0e413c8d -size 29343 +oid sha256:31e32566506ef9c6d34b144663b308a53f893b810960c50e394271ed5dc02fc0 +size 28564 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 68b16e509f..9df382c0db 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:6ee66772b20b9c1cacf1f9387c173f8170d0c132d6caddfbd389d254b2ae7aa5 -size 33977 +oid sha256:a6e129a426f95432f31ecb5776ae24e5fed7a3025217c70831dcab30ac270fc3 +size 33329 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 56510ac1e2..68d54d002a 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:3eed90dbcc789f3db882d61a31924668a42cf78e00cd634192a5bfb831f67190 -size 53659 +oid sha256:c2eb71eb5d1cac83a03abbdc6ac670cb96993da8825ab224b43e593ae4b6ee8f +size 53719 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 3bc29fbe6e..f71897a57b 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:228a9ed83c86a160fe494254ec654fc0bf1d6923e78f5de2b4057d386e25b333 -size 30750 +oid sha256:71e7b0aba9acb702371ed0eacd6d86735d47d02dfadc67e55079a454c9c8733f +size 30242 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 56510ac1e2..68d54d002a 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:3eed90dbcc789f3db882d61a31924668a42cf78e00cd634192a5bfb831f67190 -size 53659 +oid sha256:c2eb71eb5d1cac83a03abbdc6ac670cb96993da8825ab224b43e593ae4b6ee8f +size 53719 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 bdad7df7bc..c34ec228cd 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:64c4173147686886d5021cdaf6480644a5a520f81392ca84aa7ab5d705211bc7 -size 38682 +oid sha256:a1c6a823d353aa2f63633bce1d0336239196f26136b532718fdf2c49c183fbe9 +size 38014 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 f52aac7f1c..651a16265f 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:c6c73dd018eb82fe9e54ffbe38dcc491938da69791f4ff32081b2dac7549a4b2 -size 47958 +oid sha256:359b8ffc79afd24ba68c2e7c13ba551e9d63c72b787e530667cd5de5c16782f6 +size 48002 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 f52aac7f1c..651a16265f 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:c6c73dd018eb82fe9e54ffbe38dcc491938da69791f4ff32081b2dac7549a4b2 -size 47958 +oid sha256:359b8ffc79afd24ba68c2e7c13ba551e9d63c72b787e530667cd5de5c16782f6 +size 48002 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 4d2c26d91d..12fa559ca5 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:0b0e1f00b98e463a97c9334da018ba944414526f771aa4ceaea18ab192a36863 -size 28080 +oid sha256:84d3be38de70c774b36592543eb5636029b08f9a847566dea4f90979ec9be97c +size 27529 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 a650636f24..543aa41952 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:87e337a40741a5c7bdcb2015b8f82282ae69e1eff98e30641a02314660ecbd73 -size 32583 +oid sha256:93f49929a2e696bfa8a8938107d3263dcda514f34c8b5eeb6315267126d138d0 +size 32095 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 62596baf31..53bf92ea9c 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:66a0dc249d2df7e52a6a5f7cb3f1a94be011164eaf23d365f548b99f093d3d14 -size 51681 +oid sha256:f4701f103d24353378826f66e6d602b0bdf43904ef6b28ee03d25af4a1062411 +size 51650 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 f97ba2a853..68647ba764 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:aba88e94521e05328079fb2ac0b06fd97987f81694b3378f02c19a20bfae2572 -size 29636 +oid sha256:2223132d7446121d8bd37b3561fc24ce056aeffc8828b197a2be8c92ce64cd27 +size 29197 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 62596baf31..53bf92ea9c 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:66a0dc249d2df7e52a6a5f7cb3f1a94be011164eaf23d365f548b99f093d3d14 -size 51681 +oid sha256:f4701f103d24353378826f66e6d602b0bdf43904ef6b28ee03d25af4a1062411 +size 51650 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 17526d949c..d233b2890d 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:fd6d4b2d03d0e86a22c48f89f05c83b0e7622f4b6fc2a946ace1ef33e66b2e9a -size 37183 +oid sha256:7aa9c94a5f9c324a3229a5db951411fba6759aee18987d61f6ffc6d74ac79a60 +size 36665 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 bf33b63f8a..084de2c14a 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:d8f9a50b522d34a29ec48fcef545a0880ff0f43d464f1874c3486adb9c79e202 -size 46251 +oid sha256:a952926d397cab2a689e3f4096de103bbb3075f27dbe45fff311a153ab1c9ace +size 46181 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 bf33b63f8a..084de2c14a 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:d8f9a50b522d34a29ec48fcef545a0880ff0f43d464f1874c3486adb9c79e202 -size 46251 +oid sha256:a952926d397cab2a689e3f4096de103bbb3075f27dbe45fff311a153ab1c9ace +size 46181 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 deleted file mode 100644 index 3bd0c2ba54..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:51f0b3f7e4bb16728f21055de37b7b2780fd2a1fc65b6bd4564334daeab20763 -size 329042 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 deleted file mode 100644 index bf0a11f093..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5febc6580e4f0bda75a27445157ace7f1acb620c17cba55ca5d2a9330743c1de -size 283397 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 deleted file mode 100644 index 2ae328d7eb..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:34b6dfe4e65612615c3dc87e5f65bd0b160d97527c4a4749b496bf8d48819d96 -size 256641 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 deleted file mode 100644 index 553c54a63d..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d99c470fd5134e0a84b284ed32f4c93de01561e630ef32d7c22a4b476bb871b1 -size 277852 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 deleted file mode 100644 index 6c3a6f4bf6..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4b15a04a861812e3e4dee0a6a30c6afef4ab171c5261d1e5bd5a234bb7296d97 -size 251908 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 deleted file mode 100644 index 530c5e5bb6..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c32b4744750b9f18612bded2e292dde151e2bfdd8a69a36c88044f5ca3a76f8e -size 311315 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 ccca24ef6a..9b77a1b471 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:a14113653096095323ecb94cb3dd694985717b8585c8cfc6b7a45cf7a2483a79 -size 402427 +oid sha256:54298b08251d3bd32c451dbb2076a40f20254f78806c30c0647f1bf062f3df7a +size 399342 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 c511e0e162..27d8600ab5 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:95c64e1e7055dd048b88f0e19a57fe696ee93505d74f96bc4f1915fcce769d7d -size 402072 +oid sha256:02bb9e9de3b0ef480cedbed50483bdd3a899497ecc40ead72491106c6f6b6611 +size 399030 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 f229a2a82e..76e98b4314 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:4ee2ee54c694a45316bbdcfba7038391fe808f5292356748e9e66f46135a9ae8 -size 61457 +oid sha256:6e8aabdc6d15c46ee59ba0e9d2b3b3f19500801c96501b39732d7f9f95e9130f +size 59204 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 ccca24ef6a..9b77a1b471 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:a14113653096095323ecb94cb3dd694985717b8585c8cfc6b7a45cf7a2483a79 -size 402427 +oid sha256:54298b08251d3bd32c451dbb2076a40f20254f78806c30c0647f1bf062f3df7a +size 399342 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 09e08632b9..3204029c4a 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:44c7b0e86781ff6112c3be5b575b548c5ae6e9530f6f27a070ebb008d606a20c -size 61326 +oid sha256:9eba0f1d35c5456b58a09ca8370c93f0d1959e8e9aa503501cc49ecfaf198522 +size 59075 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 f8fbf8c1a1..394e42e69b 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:ccedfe061af8a77da7ddf6b20d23899bfff986fd6c74b87480b588e148ddd8de -size 89013 +oid sha256:e078891f5a377bf42cb787f436dcb7471de959db0c4d3afa5d7cacee20b2bf15 +size 86126 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 fbff934ed8..50341180e4 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:f52a267ee2aa300185191aa3e610787c58d7222bc8e4a7570879d4c5fd37133d -size 328936 +oid sha256:a56951545b00dc74fd7780648bb2a508176c47df6a2ce6920f2b8a63d15f58a5 +size 72675 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 5f90adbd8a..8f18c5dace 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:1feeb32f9dce486c54db41727d9d9801f606353008945988331c2ce391dec451 -size 75364 +oid sha256:c0008cd2827cf805678958567bbce2ca86640a5f27e95ebf1c9cdbc0b86edfd0 +size 405032 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 d7cdc540f7..43c6183fdc 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:2bfda29fcaade9508e0d742b0de7ac230d41a358e98755e4b08f1ed7d5affe30 -size 408106 +oid sha256:0b08c65638e961e2fa5f194e93c3a63ead0976b13c1b9821850c3ee865eac0a8 +size 82767 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 deleted file mode 100644 index f06abb6c7b..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_9_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1fdc4e2d82297927d2062586398fda3f3b7f6febfde8888a062229caf713ff54 -size 85154 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerView_Day_0_en.png index 1f93d989e2..6a73efb364 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3da06e8a768fb3d1987fd4dea324439f15149ab101ee7644b94d0bee98f4b4f -size 18891 +oid sha256:9fc58451a493906def731f8002457a7c88de57c4024d38aab0feb8bf9704491c +size 18847 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerView_Night_0_en.png index 71446fdc34..3a89d35440 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_MessageComposerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a14656ec0cce306c1057f56df52122c5fd2ca2525c9803e47b5845cde361c2c -size 17705 +oid sha256:75a0673b17799b239c2ca2719b6ec08abe4c4fdfabe3cc0ccda5c51a7a1db880 +size 17712 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 deleted file mode 100644 index 08169845d5..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 06dd93b269..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:578a3707102c7754f607c3a14f94f32cafa30ee570db174386533c484cb4773c -size 295031 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..57b0b89912 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:b2dea1019d3de891dd47d0d5e3b1deefeb0938be82afb72b0dc77c1f14596553 +size 144841 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 def7635efc..57b0b89912 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:b9f2f0e8ba6829cb3d83dcc0c6d3ed5a91a0346771f84adb6731ba5fdac01f4d -size 122009 +oid sha256:b2dea1019d3de891dd47d0d5e3b1deefeb0938be82afb72b0dc77c1f14596553 +size 144841 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 c6a916891b..57b0b89912 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:459141f4442bade537f755d56fe0b4569980a26b92f23409c477802462084b3c -size 122118 +oid sha256:b2dea1019d3de891dd47d0d5e3b1deefeb0938be82afb72b0dc77c1f14596553 +size 144841 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 deleted file mode 100644 index 07613eb447..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f85b9e88dca466b567769291fb69afac535135c3a07f412d6b8203968460519 -size 120940 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_en.png deleted file mode 100644 index 40384d68cf..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2f0cac0a30818ae7a2e1439d403654052bad931c8ba136bdaf37f16272a0b858 -size 20160 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..74add72a23 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:9305cafe7cca68c9307577533f3b489566c669cbd4fc08c724c8f492bbd75573 +size 58482 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 f06f86068a..74add72a23 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:00da192b3b46897c9d50986ecd84e07332cd2b490ebc8f3893a9497e72f18af8 -size 41478 +oid sha256:9305cafe7cca68c9307577533f3b489566c669cbd4fc08c724c8f492bbd75573 +size 58482 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 140a3c3446..74add72a23 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:c5a9f9104c9544e32ab0ab67ce03dcbef51e8613c1444aec866ff9bf60f6d241 -size 41683 +oid sha256:9305cafe7cca68c9307577533f3b489566c669cbd4fc08c724c8f492bbd75573 +size 58482 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 deleted file mode 100644 index 4cb5fc6e48..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c015bae5b3ce8c4447c62a13fb74d855c23b0e41bf0b128a1834d30781c0d1f8 -size 41110 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_4_en.png deleted file mode 100644 index 01b3bc1f3f..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:09893d3f2bfa2c1f5b99368c21d070ea1aa460d576722410fc2ca85c4eab238c -size 15361 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 8d62522c1e..5815548686 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:13573edc4f14581b14a0238a66b6fdc040ccc02d49cd670c5e0ec1eb78b61bed -size 48621 +oid sha256:abc1526f441c218d39e44ef3f146d4fee3bbb0628c4ece25fb2ae4ef7e4100b0 +size 48820 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 c61ea59d37..e01465633c 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:d3153640c7d5f5be8bc66424cce167c44f48497bf5d91a25963a1e2d5e506d6f -size 53643 +oid sha256:20c52bb8f44c186d3104d457c5de9c0597d99f0a5be37a9d8680fb4be35d422f +size 53777 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 e1f1510b5d..2faf0a5bb2 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:4bb4d59085cc26a284e1bd9c2e6fb7e982a1b32eae5f46282a4dde93b5d5ba1f +oid sha256:8ef807d846ee06add8438e81ece8570cf688742268af87b6985551c3cb2a0faf 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 b09b378502..fdbb4a4aae 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:e6cf51de97f040b1028bc32d167f7c4acdbd2717c3222e1a1d97929d666cacc0 -size 5669 +oid sha256:46b2bf1b4ed49f02ab2ff5218ad6bf2a7e0184e022af60d4cbeb8599646975c8 +size 5670 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 e89697a8cc..2a713b1b39 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:5529e89e00208e38522f5206f5b8d304bd472b27071cab4e0d3c2daf3cb64db0 -size 49741 +oid sha256:155ad78cfadaab78089293eca38ab8c404f227e38c451dddbbe3c59cccb82bc5 +size 51391 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 37037907c8..77fd3bfa80 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:fa29aaa82f21912dd5147ffb6fdc457fd5880e8be4abde4f0177d0ab1a412fe2 -size 48245 +oid sha256:8b54d16054565d3ba0280ff704c350227a03db0fad93750ad6d41f6e67f605f3 +size 51582 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 a0b392a659..592d4b0e0d 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:495951c4763e1c3015a88964683d384b84dbfce1b9569630d81ff8578a7d13e2 -size 293410 +oid sha256:87b78e2a06bf592ac538eecfdca70a9c5ec51f69e397623edcdcad8daaa9bfe8 +size 293405 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 97826e6a68..a84752536c 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:25d4838bf2587c2ccfffe83d53d556ec35ee7691e6eff91689d41dfcd538a324 -size 293209 +oid sha256:0bccf822697a9710752ce5323c8b6481d7080d9eba7579d9679f451818ab71a6 +size 293202 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 90e9e72d8f..4faa1f828a 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:7684bb50d0012a7175957d0bbac28f68856573cea37f45697ef3b81235f1eafb -size 377102 +oid sha256:d6982eb3368779c8e7fa602059d9af4119f450f9f2f7f384b0e1251080d286c1 +size 377103 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 c7a8f44df4..14d9ae5907 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:f6677e0cda28bab1973cc767606eedf86eae3a1ae111b552c6f284eae4fb2de2 -size 375875 +oid sha256:add275ce7641fbeba7acca740b2d0d6df0ed06c1c3c5cbcd05a1ca474b086ccf +size 375878 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 cacc88a34c..480ae88643 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:789dbfa4886cacdaf5bab7bd6e501d0dd6e31af965b398d9f61d7e46c653a8c9 -size 411661 +oid sha256:2476a6e3d5beff4360a349adb831e63fbc05962b57bb0f551ec784e28b9495d1 +size 411656 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 e85c9987f7..75cdc28a3e 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:a740b9d1af8d48d5b9f9bd47abab34ed37037c7c2646d92a05af139435f23d27 +oid sha256:8cb2e25a8f461fbb9ca9082bf94571daae1d2c886d2cd33ecea14f955eb959b2 size 409955 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 39bbd406ed..b3255ab5b5 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:6f7e3f1cdaf18d7d529be07cde3bdc9100243780101e73dd53c3d78639775974 -size 66862 +oid sha256:8a87e01d7a14fa40b73c76af0e80c4f2b43ec8cd2f40cb82ef3e321d54f6f341 +size 374820 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 414db592da..67aca77a8c 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:467e5aee42e9041db2672dc0d37e98fc1090c19207604a31247f3142d845f792 -size 493534 +oid sha256:c94c770cbd1f41f0b50d0a64290c96e1a75bdcddbd0f850d097cd41e5f2a9cf5 +size 493537 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 b9723dd5c4..dde5ade114 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:723238ad84c9925eb0edbbf225b50a03bf29e5671777d9f03eba6b910ee00fcf -size 488705 +oid sha256:9d73c338dce5b27c718cffbbf7cb1fe8f5b9d72f0d335afd81526849190d4941 +size 488704 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 6ff4840c57..b3255ab5b5 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:fbd5652457d495c3c6692c151be27c985cf47e95c8b89af9b6edc8c1b734acc8 -size 371265 +oid sha256:8a87e01d7a14fa40b73c76af0e80c4f2b43ec8cd2f40cb82ef3e321d54f6f341 +size 374820 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 e5795401ce..fc5e6d8e98 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:06e2c25d3cbd111b22bd0188e2b1880729e023170c7bed972cdb3c34544ba55e -size 55547 +oid sha256:3ac7741cfc4ec1d8fd9c8c5bab7dc3dcc0ba79932b5a1ab325573b7dd3622c4c +size 153022 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 c418921834..fa93db2f0d 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:3bf788838a204acb53bceba268327e3c2a772447490a597ed4929487716bc6fb -size 486425 +oid sha256:c57dbc66342263c4ff1d47c18099dd2cc22b752f1b6bfc853566706dc0cf5110 +size 486435 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 6ab414ab0b..150b2b0c87 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:80e198bd8cf523c785de4a66c4c22cecb22d1040377bdfd66eeed821a4281592 -size 481590 +oid sha256:5d4fff1585f602d561a98bfbd907d3c7499e6717f488eee711e90f2b0f5df7b9 +size 481595 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 825618ae17..fc5e6d8e98 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:75ace43e27a0c8f9786b0494931c3ef5e12d903e7417dddecfb34c9526e74f3c -size 149314 +oid sha256:3ac7741cfc4ec1d8fd9c8c5bab7dc3dcc0ba79932b5a1ab325573b7dd3622c4c +size 153022 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 68e8a2127e..b2c131b124 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc098d35a0af82ec65dee8ccadf6df83246fbbc7933239fd940de938a4dd4476 -size 55995 +oid sha256:e26887bd81e10726414e1833029b4b51e22e534684a230777ca50f86024af994 +size 56430 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png index 13207941ab..41b8b16bcc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb5305c7ccf69c49fde95cd3601bf2ee103988775b366cb2ee3fcd04e8673729 -size 55131 +oid sha256:ce44cf850169736008a3f3fc21a2be4fb044badfae631b0284ce379b325879df +size 55533 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 631559cabc..51846a888b 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:ee269e52306e048bbd0a06ea7c56f9080f37f37b00909c3d48a02a3a37bd57bc -size 131800 +oid sha256:ba76e7df81874aa6549a5f8ca7987046a4b43a63852c83fece541dc319e839d6 +size 131727 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..3501707206 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:b92c8a8283be1efed5faf6fb5f8a091f225dee38fd95e1a1b1914fa06661dc21 +size 56261 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 157e74f7f0..a63b53d2c1 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:dc02a7c7b38753d9509b3cb0fb2eb0e29b6d6c79b8471ba4dc7dd5b81b13d15b -size 50892 +oid sha256:b7fdd2e68114457368c5d19fe117b2d5a86a02ca475925c9fae0269ff92f5144 +size 66312 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 deleted file mode 100644 index a63b53d2c1..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7fdd2e68114457368c5d19fe117b2d5a86a02ca475925c9fae0269ff92f5144 -size 66312 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..58e652df1e 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:c89a2eeba5fa540b6ab6516da1d8dd7810ee0754149a8a7a07cbae2182d106f5 +size 55364 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..11901f4244 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:094f9dc069bfaa0a5b168d2ec41e0b3d9e9dceb135815cb0f07ba4cab9dca669 +size 54108 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png index ae8c29241d..6e06afc836 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:616deb9421b09e2fa25d6953263fd70a9f687fdfbdf9503bbc35e7a0660ec440 -size 58459 +oid sha256:832b227fc82b946df042658d134f1c699c720d3e153576259fb145ec9d0c4c45 +size 58441 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..461d173e97 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:407301c3ad51be44fcfcd804954a672e677ea1bc59af39e4269100acc4f720d7 +size 59368 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 e4a14ae551..b8d8a2cf4c 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:39430896a687266d7a60890fdac5dbca6d36c87fe30d07619c9c50e23c3f77be -size 56972 +oid sha256:1cb21bd5e7d348d1d07ca4b6a360f26a4735cc9760fdd7e3b4b1bcde32da6f08 +size 62655 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 b8d8a2cf4c..284f92cb26 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:1cb21bd5e7d348d1d07ca4b6a360f26a4735cc9760fdd7e3b4b1bcde32da6f08 -size 62655 +oid sha256:473a4f7d9623a7351399d3fcf98e30adbd31feabb23efacda83fb68460b75e48 +size 50912 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..33ed4fd667 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:ccf988123f7fcf7a2d6ad3914d3e1a30dcf99c49ada7fb7dbaaacb64d7f2250e +size 55562 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 74826e1e9e..d2b5044048 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:805245ffe7f4e99a0f5927b89bcaa6ab8eaf7cc603d352b14b8109e76eecbdf2 -size 51742 +oid sha256:52441a5e250b027ddd40ee32754a63c6206762084f47ecfa7e057e2ac77e78a8 +size 69120 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 deleted file mode 100644 index d2b5044048..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:52441a5e250b027ddd40ee32754a63c6206762084f47ecfa7e057e2ac77e78a8 -size 69120 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..f926ca104f 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:1612a4b0e0df958cf99295ab318ed8260d5c12b705f65f5db4ab2341930564b8 +size 50518 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..bc016b9861 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:7078a6ac34df84c959f6e2c206d818822a58493673749a3b89500a1ef1c0acf8 +size 52949 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png index 4f903b55b7..eedb0c1ae8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e99ae9731e9c75fd025db8f24649d6219941ff3aaf0bd4a67bc2c8d1f204cb3 -size 53516 +oid sha256:f69563c42d5315b9e4203719128340fab045ea3473c494e1a1376fe4cbb3f0d8 +size 53521 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..1b03d639f8 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:c623703f53cbf1ae42e1d05af88e3c94e5b438cb96a3dc5cd234026b29bf0e0a +size 58286 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 1ad21c4d64..6e371532de 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:289819914863942caad5202131efddd9b880b4af64a36d4b45fb547b5b1ec47f -size 55916 +oid sha256:86273e812cbc6245527c3d1f138111ece99375aec0d68728882b656b01687bff +size 64392 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 6e371532de..dbe5409ce1 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:86273e812cbc6245527c3d1f138111ece99375aec0d68728882b656b01687bff -size 64392 +oid sha256:ba5ae56e80ca9f5d3e5e2e0d50a2cbda7870c6286f350764dac7817d533d6c18 +size 51735 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 deleted file mode 100644 index 08966f9b39..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:42fd1d9b089b026a9edf1a0ecb71da47a4517062cc19f61e16e70f9b3276ab30 -size 59658 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 deleted file mode 100644 index 25f2f73063..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:314ffd2ccda8d0315a27e0cf08fa6d158ddf38fa0903e1e8d7fe45361254aa0d -size 59525 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 deleted file mode 100644 index c9b289e9d9..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dc25ef0688d2aa9be1fcade1dfa282b243cc3c45bde01d3571b2ad9278dcda74 -size 59509 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 deleted file mode 100644 index 3398dc3826..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:229b570a3c938e75f5d7b33c7fec89be9582ec06aa85dfa587b995219b145e88 -size 59517 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 deleted file mode 100644 index 6767c03276..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9c9caa916637c212f2cf1e5d175a9cf096b83606e1161b7c2bb1c8ddcb142e48 -size 59363 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 deleted file mode 100644 index e9434a4041..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:296c4b7449992b4aaa478038db9b6dd4e687ca05f82cc351db258e3f178adb6f -size 59656 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 deleted file mode 100644 index dcbfd047c6..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f13aab730039d52c1d92e5ed3ad601095cf7c8b3bd76ddf2303f2006ea6f6092 -size 59193 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 deleted file mode 100644 index 3526e1e6d7..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:961959403b1763abd537e05865d86475161e650e61e51e65202522d08faad55b -size 58771 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 deleted file mode 100644 index c1707ebef8..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e5df1191bf18d6c922ca3d2a22584c98c4b871254512ea86cfb324fddf8a2d0 -size 62520 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 880c259789..9feee4d98d 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:ad35559b964b2d1197b256348e175a8082bffc9247024a546eea86655c9c0d8e -size 56855 +oid sha256:b832525da2a7744eff8165c842652bfff4742b728b000b1c8ef8be81bca75efe +size 47052 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 c3e7180e3b..9a4a40c0e9 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:94b94eca0502a081fbb862c426b5063275ac948c8d998e34f40e362a82591d75 -size 56707 +oid sha256:8ae8264008b6396f31332263472c910b492fffdb9dbf7d0186b44a272476c02c +size 46903 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 bb6e078571..fa74fd4b96 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:46bd8afffd3a9c4cfb92c7d9ea2c2d0a3c4942115a80809d6bbc16037e2839a1 -size 56705 +oid sha256:4842f6e88df23ae0050f0052ebe9581d6c19ac5e29a0c553716012713fbb00aa +size 46901 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 c0ea01e3b0..5f9af21b6e 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:5fc17122d1466b14e85a49165bd3b462756181ec26cbce280437f015734c8e40 -size 56716 +oid sha256:b7888dbb0ed5460c54796a6b2c232945bd4449e57feda4aed89ddce097a3c35b +size 46911 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 efc3e893a9..fe34c6d5c1 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:12cb6a8ee6846a0f44d5e14599fd7c6dbf97da97d35c6b6c83545d601a246efe -size 56563 +oid sha256:215745acc9dc0c86e239acf614c94f1cd46e96cf1b6dd7cdca514d7a4a82f835 +size 46742 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 7ee27433d6..9df6b272e4 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:1546f59a5be6b5f8883729c4b83005ebeb990188a86c100ba384da46b2a1da7e -size 56853 +oid sha256:f5d099940f91cb3e520f6eb6cbbc075bc57817f3e46e3f9fb169a36b408388f1 +size 47046 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 aae9546e31..f56d4e2115 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:714782c0f439f933f6e0127ccf54b6e09138fa23d44a3596017a23665131c119 -size 56498 +oid sha256:ce9c49a9d136d3bae5fb366fc47d3f48f8fabbe6963d0e53166dc1a5ec4cbc20 +size 46695 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 efadf6c0dc..f19645feef 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:9a58efd0afbc84c88d8635738d3bb25826c4d25d790d4eaa1aba0e9efafc3b56 -size 56043 +oid sha256:ed86c0b3137dcd650d7ba65d7ec009146234b4e0b983b1ab8b56974000698201 +size 46242 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 1547c5289c..2b725b9e8f 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:9d502f4f9ccf2d3eff614d9ed2b751a252acd05c1bf5cf4a8b11775c45725c3d -size 59427 +oid sha256:dbdf230e73c3fd93787030d6f5eac65b6a8131bf2eccb07fcd8474f191b87b06 +size 52563 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 1cea623bff..f9a16c247a 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:7c80e9985aec6b183e33f12ecb884753d1b8f67927d6d5713ea946ea8f5d6dd6 -size 59300 +oid sha256:2d57215dd58ace85ad235fb7f95ef592bfddd3c21afbf183bb5d31fbf772b69a +size 48900 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 d32d440b79..e662b2dfbe 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:5dedcf20e368909de0a6cdeade437bff06b38a1f1b6aba3355968983f1387018 -size 59201 +oid sha256:32926b81da50838d883edfd157e7d12cc3d43991556f87e61702b74e693ab6da +size 48798 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 6f672c23b7..5ac95223f9 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:a9023d044daba4573bb9a3ed4da598bfb5c27081e4f7a75757a4796bdc44573a -size 59207 +oid sha256:0594480e59710892e480a83c1352a501504ae1bafb4c5c6b672c7dc62b3e75e1 +size 48801 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 54bd893d1f..ca2e552c10 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:77eb9f159dbfa30e6f750500c2adf21b18cb5f1dfb7a574be68683db24803225 -size 59174 +oid sha256:ba828796a14ee9e82e01e98a89067649a7f3438a2fcc8e3ce5f69e12e86f86b0 +size 48777 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 e945e52dfa..6f741da48f 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:3dca50c7a0f0d7ce96388a01456871decc76285384d2573161773219c79fb876 -size 59060 +oid sha256:3ebb88fa29993c785e6ca82543e7eeafefe365c0018a4f22852b988dc35db4a5 +size 48716 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 02c571c605..1a12af9468 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:fe4181c3e521f1f788dd06e3cfb1ca078655852415dfd4f26f5480dd47bf084c -size 59303 +oid sha256:2521e5352ee6030f7a0fd8304b19f6a7a95fb6364d43f412eeef03799c8c0f76 +size 48901 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 7da66a622f..962991ff8f 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:18602ce14232fd438450782a564354c38ebffb0c56d0c3887d88c47cae53dacf -size 58962 +oid sha256:6dfcf074e7fa4813218f4bf9e472f706c5581e6fc5c1f7ea1eb9ec422172190f +size 48713 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 015ecaf7fd..8917220310 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:c3ae0af5031f7f850c40fa6560aa6d70127e006931a3e18cbee04acd7c7aed57 -size 58526 +oid sha256:a3195a1871862ce32cdcf068608a565c56e794ff66f14473d5f6355918db79b6 +size 48402 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 19d1ae2ca8..5fc76943b5 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:a980564b6406bbe45b9a170a5744e45c9e019556587d433e5fdffd8ab6f1c6fd -size 62034 +oid sha256:f71219202890afcd4f43b5f57cb4d555b723df48f71848b64c8db415067d1d4b +size 55044 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Day_0_en.png new file mode 100644 index 0000000000..29b7fa324c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df19ce5a967143e2cb6d1fe021663f72e36f20c32e912894a2fbad628f03c3e5 +size 53561 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Night_0_en.png new file mode 100644 index 0000000000..2ba5234dc6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_MultiAccountSection_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fd24e865907b5c9240829710a910e445954bef9b8575f5115a52837e00d817f +size 54591 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 9d8eec80cb..bc97d5a5c7 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:e83c8d052ce340010f97c4ef2750e4b921a129639a0004bd951f71c7348622bf -size 40597 +oid sha256:369c835c46e19d3e3171add57055624cf672a9d34109e6c831e0c1bce234c605 +size 39513 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 b04da30f28..c6b82b8385 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:0a6ddec2c9743976674657e454cb14bb479d774f1aa13882a34ceb89ff75b179 -size 24424 +oid sha256:3d9f6763de5b844eeace37bedb25b125976625394d69d7843eedb26319e926aa +size 39316 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 deleted file mode 100644 index 18869e3590..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:95e30b24d5a6571cf42a707a01f0458cf509428c9ab8333924e1679f6d4d4f31 -size 35778 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 deleted file mode 100644 index 2786b0cb8d..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8e54c2c665235d6b4c45161b4fcc0b8f0a96ce7620bbed9f56e04ecb71916f3e -size 26640 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 deleted file mode 100644 index ff630d5aab..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c2fd721612e0620c3a4eac0f0835ac1d03ffb36ec45477340bb732ff1c63cc30 -size 26921 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 deleted file mode 100644 index cb6c51d959..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7c7fc020ec4bcb2f67a3a534a621e71233488b469b24e6bd6d3efa42f9f944a1 -size 20263 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 5fe3875d72..dccf28ff97 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:a07bd9ac57c1fb6074b81134edcd169c2c7edc3dd19039766dabbfe8ccd5d8fb -size 41621 +oid sha256:dce8486726293027aedcdd2e67d10a39a1a2c439ca67d81ae247b60119675ada +size 40385 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 a2d4d1b036..6f2381c6f4 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:fa1a7c175f6551680b1a71ddaac14014f6503ccc2928d4ad97eced8a80626ed4 -size 25158 +oid sha256:93ee581f59c79e03b9c9311765da4c828c5009d14e92f7cca9bbcee418fdfc63 +size 40442 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 deleted file mode 100644 index c188be2848..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:67bc6eac2128230323aa7dabb6896b98c4249e6fba9edb4d1769f36dbe031135 -size 36346 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 deleted file mode 100644 index f2ca7b4a6c..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a40fe5dc4a671ec2384265da36f3d42a3cadb2792823308a62dbd82423110652 -size 27552 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 deleted file mode 100644 index 8ce81e889d..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:007aeb20270db25edcd3cf2e1ac8e4e0d714e08a043af0473c2263c1e79af452 -size 27634 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 deleted file mode 100644 index c402bf81c9..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d644bc2a62596b923d782150334f60b1940730824af0b7a304d2fdfce85cd0e6 -size 20493 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 03d5ad5880..4c4d183956 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:0dfea70e781debf7a293aee2967c3da314101020698fee97d3551e25f5f378d0 -size 10655 +oid sha256:5ccaa7f88a9e46bdc526bfe3d5c2163bf3d963c0661179db97f62559edfd3189 +size 11042 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 3a1ad156a2..0edaae43b6 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:33482ad6e5033800e911dab44d4ba5bec15cfa58749102c5da4e2c57a8e760e8 -size 9783 +oid sha256:f37e1587ba12f9b6326b5b7398982fc663ca913da8c0ee83dfbd5e9decbd4362 +size 10906 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_2_en.png new file mode 100644 index 0000000000..3a7abad03f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b494ecdb4962772dd548339a4ac57be40b273b513697ed6d039d1c905617d54f +size 4987 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 27f51b2778..741a708fe7 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:62e127900d00756ecc806eb9d8efb6a11cd9cdcecae6b4bab643b7306a06be2b -size 10435 +oid sha256:bcb0063babe7091368af6b5bc7e8929c54ea879bd78043d9128db2dcea9d79fa +size 11191 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 81d4ceda60..c4e8dfdd29 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:e39bc143f7ee45c47ff57bfb53a773d7df2ba14568a9368643d8bab9dbbf4955 -size 9675 +oid sha256:f34e63a88464ddc817d1ffe0324352199ae1821dfa846cceac129a656ece2eb6 +size 10911 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_2_en.png new file mode 100644 index 0000000000..17d1ff9d1b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0618a9f769e15b4e682d763224cc1fe0abf62c58f3b9a6b4059153f8805671e +size 4740 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 3c898e8ab5..1dcb5f4f8f 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:17a23d0c9d2993b81edb72f19ba6ec03f24b79a2f732e7af6d3594c96503723c -size 24978 +oid sha256:bc7a2e36694227df123681799f09bb2ee3dae24258679557193ae69ecf4c2871 +size 24012 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 b3f17cafcb..65a0322a88 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:e2d914dbf6a309cffcba36f0affecf19287ad06c751f50b23a51e9df23bfd12e -size 23859 +oid sha256:18bbb07caa3cae740bb80297ced37fc3e8c92b48cd0935f716a86e0664737a75 +size 22815 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 b8fb7eb092..77b4b1e222 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:eea615f75cb1c3db04cffba9798f6662376f1c1db6825aa73c2db6f10192d23d -size 26959 +oid sha256:31a1989c17263d25d5bde65a7b0399b58c9e77d320654ac9f0e65f0fe2410b15 +size 25882 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 5bc6eeb700..d8fdf936f2 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:56c84d3a772d03eb016807b886df9728a5fbaf07e3fc6dc2ce4353007aec405a -size 25726 +oid sha256:28f410c8068e0d7586af4c727ce5ba131e56387472f661b34841139e1e43cd3d +size 24788 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 25302db21d..3df4b3886c 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:d7c187e173ae6d02d6c95c03bcc24cce91b0922e6aa5a08677732748aab0fc30 -size 85320 +oid sha256:dbbd62622843fbd1d8d35b63c7308eaed46b488e6b189e987983144e5395bd09 +size 78972 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 739ff5f268..44e3d87eb9 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:8979ec6209ad6e0deb6946e00ce227f3db41a565218afe31b72b889bee224daa -size 46268 +oid sha256:31d18a5e250e531fd7b986690306366cf69eedcfdbaba28148e3edd1d36ae597 +size 42912 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 7609f5e725..449dbd7465 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:16200632461dcbbf0f6211b7064b61a1e2fa04cd34e14946beb12b9e9f4d7b7f -size 43901 +oid sha256:a04a5aa9e35df5eb82a0a708b2b743bb83a1a9408fee91beff60d5a1ce2c6d5d +size 41599 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 ef1954720c..279035fea1 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:2279893a284e620cf487f6370982a14278fd57bb278e53932a5fd7c5cab8e95e -size 43033 +oid sha256:ef69f889a28afe5704da86296ebf9916a7af1de751eb4c1ee4995ca3f70ea12d +size 40737 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 a0cee002e6..72f8640e83 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:5d4da60a94c56b13b65efb797825c54c1e107fe4440f56b1fcac5d47779cbf64 -size 44502 +oid sha256:1d4312936ba67965a8b35e714bbc3014214320311c5d077d33382ebb5aca5738 +size 42172 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 eb330780d8..7c1059539b 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:ee2899f7870b09f287b45b327d5d0462b423ba65afa1087dec1be862507378de -size 44411 +oid sha256:4b07f6c942ab083d94c22218010b4aa646cf03e87af41fbda89b16620f7b9752 +size 42079 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 e09c2dfd95..d7691fa7e2 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:78ba2ddfdfc47febc0503daa0985a5fdf04ef15bdc391b4a88c3c630b1946b35 -size 46004 +oid sha256:1b6db26bd05f206661a5707a972cb6f843eef82c76e5e0035775625b4a6268a4 +size 42640 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 cc49f40494..9e68ee9859 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:4a832b0e395498b15e176529ea55c50ed129f6d2330fbec4ff1f8e3dcaeab93f -size 46543 +oid sha256:5f77eba1f623f67eba427312523def9a45c41581dd4e047701b2c75f787fcffc +size 43178 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 e8c0ec1046..c42d3ebe59 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:d8e738bd873339d195b4190f65a18016f2f0eb1bbc9f513a2b25467cb5f5828c -size 44745 +oid sha256:a62965a57fea6665739d8e8b2fda9e15abf119ea59f6bbd5d7d8a269512064b9 +size 42421 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 5098ecb3a2..45da7b512e 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:892dd301ab08c6a302e8286d3f86878178130a22baf550402f2102ba98f0d21c -size 43982 +oid sha256:39780082d7826422d70902ac2b7859013c30b41d31f20aefc25ada8aeae196c2 +size 41684 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 899ef403a4..029a2c3639 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:809c597757d6f1385803c1378a494f24309b533168df59c7e06852c1ce34b8d3 -size 41408 +oid sha256:5917028dda35add5ed6b32fa9017a9cdcdfb8d273d230aa6d529bebfbf0b95c8 +size 39646 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 0625fcfe05..55a953b2df 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:3756793e5d92f0fac18e96a49e69ac7ed6901c1195d3e6526088027405e82dbf -size 41361 +oid sha256:f65f298b3f14ecb0da53f03a20a5464a7f6ae138dfdf417ad936601a521fbb30 +size 39601 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 346c9cf052..7f4cd39731 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:099f571485ce8476d4b8a2313610b6c1d03a7d7420132e06d6258b483d640a08 -size 39130 +oid sha256:df3ceef42a59cc072e3aa21c89e9b90f0524ac422d1538804b6fcb8abf97752e +size 38090 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 a58193c18f..7cc47cba6d 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:66fd2cfb9f343c77b8db0a4fbc0d7a1571a4dfe1d70c78823a4ba03bfbc846eb -size 44934 +oid sha256:c66ebe3c16933495fe33596036363f95e20654b6ef5ee822f594a34531ea640f +size 45211 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 37bb88b0fb..8d4880b59f 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:465ecd337f705262c8f533b3b01daa6a548c6ee88752be405609660ddfaf56b1 -size 44676 +oid sha256:de5ccdad3e08ed4fd4330697ad6c7085d5553cb889b7b7c12879aaaa6b34c1de +size 44953 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 6356cec349..52e66c38c8 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:3cca233c171b6c6709a94163ca1f0024a42ff66200383fdbcaf57b321251522e -size 44390 +oid sha256:e215296d864d34ceacaa53e7fec452643b63d72b253074eb50617bab50915ad1 +size 44670 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 3846aec585..23ccf2249a 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:9e5e8b39197d115c6190b89fd54033b3319b36dfbf5c6f152dd84e6c79147bbb -size 37877 +oid sha256:0bf5c596196e64554117addd1247aa1e5be6e8095d85a595b1c2a6232ed483eb +size 36591 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 08ca4ad1e6..8a7f2e2d75 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:4899d2ffcc6da1f414f7fb223440b8463250dc5ac07558cfaee2947f95c93362 -size 42236 +oid sha256:b0cc874a4ba2b8bd0f0b7e290d0ffc67e82d1222fd287360c3321143d5f87ad4 +size 42369 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 062c440463..36de539c48 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:577381a33e7f0a88ceebb7f2be6528c3e99417cf1351b74c134aab948664cc64 -size 42149 +oid sha256:6900db132e6b79de1c5ac986666af46e3f7ef65a9c4cc7d847dde6962eadd129 +size 41352 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 1ff34bd6e4..c5d81c75e3 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:b2bc2699d8dcf770103726c571a6966eacf173cbbad05c4fa6188815aa9ae231 -size 41052 +oid sha256:b7199817c8591ccf11241204af8a39d2ed1a60dfe238b06387749d57858cfa65 +size 39288 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 c1decbd918..1e0b3070ee 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:9cba49398954a404def6b37f3cde057cb88d6f59249b943f1689403ccd902326 -size 42024 +oid sha256:0fac98b00a99771b186e8146b98ab63602be7b4c5cc9febc3ca821c1d5cd6a1b +size 42700 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 379f61db22..4b9b30b2ba 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:405a29c158295616560152c5790f189a8019bd635a53a69b581c8aa595e6e9ea -size 45494 +oid sha256:ed4073d3b427b2a314667f5118b571e5896f1439582b9209077ec9c62ee0e061 +size 43097 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 d0ffc70f42..60200cea9a 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:3f948e9cc2eb7bc73b0441fbdea72d582a042895b3404c8302b7e182c3481fae -size 44425 +oid sha256:c68c895b189a41c7edf8fee58202560ef47e0a98082ddc3a346546f4f1346de7 +size 42100 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 2894ac3c71..f9287533ca 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:b4702b18ad5d203050140e6e1a18553344f52eb5482fcaf6ab497698b9ec59fd -size 44432 +oid sha256:e1879c3d54dfed55e4bb46d967c9c8c5a4f95f4e31e884afde4c9c517f35402e +size 42093 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 afabfcecf5..bb868da01c 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:4189b58dd1263a81566ea86d9881c02f053ecb2a91ba9442553fbe0f230e6860 -size 47105 +oid sha256:e2b5dc5cd20ccfdb7e6a1accabf6e139ff343c16378af19232168f835b1c97df +size 43650 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 9f07ad1624..72803025b8 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:3fced9019f81e5804ea57c0b1a2750868626ad35b9a7227009a796d289595556 -size 44659 +oid sha256:f9c7dce94908b2b0287c196f564c306a05306f058c71d75be3b298a45683bce4 +size 42321 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 74c4a9eef1..9d2b424654 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:be9bb839a503d169d67d4332a133ce2b3d76b3ff4c52c063436521f8a74c973e -size 43894 +oid sha256:743fcac827e8df7742127491722c3ce617a54c0b64fd3336f939994b45376966 +size 41470 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 bd85882fde..dd489b5147 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:82c2781c37b7cefa198f2061a2c8e900b27bd57653db88733be5ff55dfaf5eee -size 45251 +oid sha256:671ae1ee2f065705b38063cb354c4bb4a63247e01e1101dd155515b46decb660 +size 42926 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 cac488f3b8..d5f14e286e 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:36feab599a6a2d4269b68f42149a6f82832ac0f6f0e9f01fb49adc21b3a58d93 -size 45172 +oid sha256:8f352ad92472cb8b152ef0660c7f66b55b956ec05bd11dbeb2c9aa543d0db2ca +size 42849 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 3e7cfcf672..cc8b1c74e1 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:04bedd62b5ceba766197dd18da9efb0ff086c2c01c9c19446f8a3c42ed43ad87 -size 46813 +oid sha256:5742ffdefde6a9e3f01ba021a75eea20599cc520fa4976aa45cf1c10e394f62b +size 43367 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 27ec9f7a25..c9db5d9a07 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:828b065aaeeccd826e98e9601069819d7a75ffabab9820a834cacb627416edc0 -size 47409 +oid sha256:0f6be8e586f9692013ce38c483c42fb23f8c135593e17a72f3be19381719fe6c +size 43967 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 f96e1aec74..6b9fc92803 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:4740bcd118f8583334dd1c03b5011e33c5384ea6810d1c83711f538cf9f1d165 -size 45521 +oid sha256:7b4b470aa3f3650c26c6e1cf2491ffb7c3ea58db28dd00c0755a7f5727dc60f1 +size 43192 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 4d88149a93..12b5fd1b6d 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:7fab53a63021ddae8384aed8eaf0a3803d06e8c80645ecc13f79a7f5cced6e68 -size 45024 +oid sha256:656c300cea79355aa3b35cd59666f8e35aa4c09d69dd422e816f256cbe6d420d +size 42685 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 fa7e99aef2..4c7d16ea22 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:89a2c619412ad9ca43c79cb76ebe72b827da71743dda7044a9b44de2990f160b -size 42388 +oid sha256:93fe116bc34f9cf8ee3da2317948a98405e36a6fb98a58b2046a049af435e913 +size 40373 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 9b27b61083..33efb5e55e 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:56ceed440bb51039ffe7e4419477a57a235eed153a0188838e6265c241896a82 -size 42262 +oid sha256:09f89c67652965407ea147aef158d0b3281850b95cca35f7aec207693bb0af62 +size 40242 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 0542085126..5e8f3baa0e 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:3b03e9da2d73cc0e3d0485b73470e576d7ebd4b8b5f375f2a478f1c1524772fa -size 39948 +oid sha256:04c7f8e85d58591c3dd6ade913b9fe0861ff7f774fdb4b46e63c9f86ef5a918a +size 38959 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 a320657a26..9462f1336b 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:a686a2596b9b85b17c6882aa986b12e13a83a47ed217c4eb6de6aa2cb3ec2d50 -size 45717 +oid sha256:389fcb65c5334f31b3d66731de2c65128d0a17e7380ca7c45be8a36023719732 +size 46097 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 920b670806..f1f9639eae 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:d6bda369c1754e2c07216f4bff0285e017f87b550fe0dc6bb5f127845d52c9b6 -size 45462 +oid sha256:567cefb525795b66704ccd1738bfe08247004814c947f6c60801588304c768c2 +size 45835 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 aeaa04b269..fb830369a5 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:285045769d912e98df0371c829affd925642c8a20cd0b6b09dfa92ab5f143538 -size 45131 +oid sha256:f87512e18ca91237141d97ed03f4725a8a671268daf50f0a8049ee34bdc42a61 +size 45503 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 e7fe0e7cef..cb6e8c1f80 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:463548e170b48509d68c0993acb000748b19921e5d4be784b07ce83252c8a5bb -size 38613 +oid sha256:27918a9baa05b2cb8dee7219b6e19cc10be1421be190d1e35724d237270761af +size 37382 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 76eb564d8c..a64f6a2843 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:c1d19c2b8c6b3c80b68214da2bb9f54a6897a279aa70e2d8cab88c291332cece -size 42980 +oid sha256:cfd96490ce4509620efd7be72e6ecca219eafe73050417c0c3685339444457f1 +size 43060 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 40ab9e310a..a8894c7eab 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:2057479cad5d3b9a89ec835ee1d06705c6c459f7810c2875bea26b975944bcf7 -size 42960 +oid sha256:8ee0a79abd45fc9573e64b6f06491f33dfca34ddd96c0dbd3d8cf7a9052b47be +size 42072 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 e549a92821..cc81464baa 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:073addf26e8d0b638bce93db99d1fe9740abd89613a1eb8ef5bb4324e489d6fa -size 41924 +oid sha256:e7146d365a105c86eecc34027d2b824e90ca1615fcf77ca0f49ed920dbffe22d +size 39903 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 29a8b0b4b1..2f72417b73 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:56d72cab4a9d7af568cc497b68d6ca9055874544cfd462c93da37fe368801fdf -size 42984 +oid sha256:dd455b5b492eb88bde4e985ca01ab97e23a337d7ac478782c09dbb10803960f2 +size 43430 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 91cf700f52..55d8ab9869 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:9141f5fa4e054dc5cf70126309830bc38d564d467a215eaa1130f3601b15e761 -size 46350 +oid sha256:863c5db2d8fd863b057d0ddfe26bb3804d15126c2fdb58fc03c0087d79c6a3b3 +size 44056 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 fe0b629925..83b9c7131d 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:a062b8cbe4dfbd94a5b068b1f7c4654ed8b697a7e98ae4c2bcb6c3e5bf473b49 -size 45289 +oid sha256:16b815f5ec48c101a7bde78cd5a4e30e825138f359e415699ef2b14e6ea23869 +size 42968 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 26d59539db..faf5081313 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:46aafd39e1a93f8d357def4c5f3f024ad0ee13931ebdb9f72475dbc911fd7532 -size 45266 +oid sha256:5c47ff32beec1987fdb9f92ef6e5c9b7eca04ab20ebab576a2028225a595ba12 +size 42911 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 fd32767a41..de42fdfe94 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:218a8bafb58855056ab07b05d45c87df482483c02a5144c0117c1159135e924a -size 28637 +oid sha256:15f9aa77f9f8fe09a10e2e22b194f2ba505d56c9097db0872b317840c0fa84c4 +size 28661 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 a3c91c139d..6ded082e3c 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:76f1e9db9260cb23bd80bce5f45bdf323a8d24cb87757d7a32f9c6e6b632913c -size 27993 +oid sha256:4f1762442b6dcd37d5710437239055e7d40d2b741cacd1045a3eacd327fc12ee +size 28053 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 a3c91c139d..6ded082e3c 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:76f1e9db9260cb23bd80bce5f45bdf323a8d24cb87757d7a32f9c6e6b632913c -size 27993 +oid sha256:4f1762442b6dcd37d5710437239055e7d40d2b741cacd1045a3eacd327fc12ee +size 28053 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 6c8ad3018b..88df849ef5 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:4fda40268a68acf664e90a48391658b8dedb3d7f5d5e6b76d680ab25180d5a55 -size 40121 +oid sha256:ea5df9ecebf686ac65c265d1b74e3a0a5169d1e308eaaf4f9c205a754d8bddcc +size 40150 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 4317869283..58a01b25a3 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:6c8f9e633578f46a6b8126ea6dcbdb5c113dd0e1107fbc4d5b2206c2f5acfb1c -size 27835 +oid sha256:06170ddb862b337c00d50a531caa1f673952cc0194cb38410d3aa7cc61d95c16 +size 27653 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 eed9b50326..f346d344e0 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:d55afb5f76aac3d1a8ff0f1c223502fc798dc0684f9d84df79256c41ca5ea1cc -size 26706 +oid sha256:fc1a0fd307ba6ea9a9f578527bae5a074e5ad67035201d8aa7d85877ee697af5 +size 26690 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 eed9b50326..f346d344e0 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:d55afb5f76aac3d1a8ff0f1c223502fc798dc0684f9d84df79256c41ca5ea1cc -size 26706 +oid sha256:fc1a0fd307ba6ea9a9f578527bae5a074e5ad67035201d8aa7d85877ee697af5 +size 26690 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 0b3c962498..f38139bcf2 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:9410147c7a6a496fbd356b0dbc3b14998f20ddea6a2eeabb79b9600485285a80 -size 38628 +oid sha256:763ddaf10dfed74dbef17deccfa7037d8b643e9b8654409ce96e8192ab5ee38c +size 38418 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 e1573dacb9..21b17d8656 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:01d03620cae8382ee41703d7553913fbd1e463fcd55fd661e9b99087e14bf6b1 -size 42162 +oid sha256:562e8457321ec1afe9ca9a2111ee19815aa25401cd925c0bcc880a25db99e0c4 +size 42100 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 bea6d32d38..c26e5c44c1 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:54d9e8714cbbc3785ccd25d6bff9c791d57acea007d6cb8774a4a5d61646b54f -size 39857 +oid sha256:4498edc796206821f346a2dfceb966f59f4b7adf513ae184c84d20b323ed0d67 +size 39800 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 dd9010b013..e798d07cac 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:9f6ce15f59eba49738b9df2117764e760f2ddc06bd52f8386f6d78af307a4104 -size 34630 +oid sha256:4f3d6bcf6b603ea2e2a619f569960438337fe9e2ae9eea2657587cd211bbf278 +size 35444 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 3264a61179..fa0b46361e 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:9a0a377b34502fb55b3ee72468533914de3c68df62531a6f40a41984c94d0047 -size 40996 +oid sha256:b9c17b8ca9966e4239e5c5fb9d9e5f0f061bd8d7fbf3bd9c76ce6473af537d05 +size 40962 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 6d6a189589..0072d95bed 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:433de908676d053d3728b8cfe03636d082691059dbe4c15b950483cfdc3e1ce8 -size 38634 +oid sha256:6e73375d44fcd158c7cc2cd0f97b49c8a0f2d523772cab58778ed7b31a8a0d5e +size 38602 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 0d1e17b957..b8629728ea 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:56030e621f122f288b00dc642cf5ab4a3a09100cdcb92e068d8fc0f47f9a5328 -size 32421 +oid sha256:7cb10d8ef6b7c6fd25e772428e03755f1c731197660ed7123b79c56629f74583 +size 33086 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 5c0a10cffc..f3d5c2ec82 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:61b6259baf820caca8eb8a28052142c56ee88a952148e204c25a53f709b87fe4 -size 44542 +oid sha256:cef767ba47c98c5b693cfadef2ea0a260a35d3144c2dc26a3bb52bf632700d9c +size 44141 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 599ce75592..5c91aae383 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:2f246482f43b0298e8948925fda23d9c306f56b484d42eeedbca1cce2dfb6bb2 -size 42382 +oid sha256:2ef46c819c931938a4fd856bb2fc697c964d4445d6cf9f8b5e2f341f22805061 +size 42001 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 224e1705c4..214e107f12 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:2ab51540f79f485e98b311210fc7d8b98ccb174b9e689587559878c245877f4a -size 38492 +oid sha256:de188fac87a59e6765360282655c081d7b93e7dfa47837b0c56dd77a9892ed03 +size 34616 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 654f566376..7f69976784 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:b9962bf5c4f9870d51cd3eb3346bf0e2054c5f7e21111f57c7a7bb1938e81aab -size 43211 +oid sha256:6b53f00607190915bfb683c0bef1bd11f0804651fe6bf57bc9312afa20768a75 +size 42924 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 564035b900..20dcda7f44 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:fbf5c303797332b7419e1529ddaa8d1f2f1cd6ed2086654880c7702fc3d62070 -size 40971 +oid sha256:6cd74bb51213ae932addc3dbaff2bd20d877a249c46ac5bb7656e03566189b38 +size 40748 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 3d21f69991..82f91a65d5 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:47fc45150924e2c1aacc86889824f7c0d7382af94ef420ff690871d75c7c3e23 -size 36212 +oid sha256:dcad0014629c828846e05247a1ca0767c45906bc15378a00cb9d4e6cd7280dc1 +size 32380 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 de213ca597..cad7e55b37 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:80e8c8bf0bf9b34d3de0b0db7f67b84a667a9f510d6e9eefc708de25dd1ca5f8 -size 76725 +oid sha256:c8af941ad8e5b553b82aad8b39abd8b4876d67b6906acf0ee8e4715673d8b35a +size 81462 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 a980e0c914..b5e9457b1a 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:d7e8bc45d019ed73b4795f29b36c28411e8e20cd4bd231e76e8619e011b64b31 -size 40203 +oid sha256:f776cecd5278761fe7904b63dd980804a6dc2d3e7acc37fb7ea71d6a1c1e63df +size 42505 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 a95c48fec0..ad21975ebd 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:1d22817145519d17475a09dbe9f5e3a71ec6b5ff9e917d7a92b06feb3ba865c9 -size 29049 +oid sha256:a2c745de02098d9e38f75fa7c34f436ad4f7770f4d0cbab03949e0f3c6bfc958 +size 25557 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 33fa7369ed..a2f44ea5a0 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:27249bb1a928cb325557c8aebf6de344b7cd7a16d3420e8711591f20a3eb36b1 -size 18947 +oid sha256:b094f8d20b4b946abcd95bbb0c1d64a0190a5c651b6f434dbd9aef2857a315a7 +size 19991 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 ac26997f26..5e3737d938 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:a5f0abeb2bfbae8fc6b864ea2e6a90b4ab3bcfd3b9f5fa4a06c6a314bd194a5f -size 25660 +oid sha256:a3fde1e89269c46ac8a6b4f0a05aad45bcc806f2a1b6108c09f7078209f7799c +size 26574 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 d0d1748a80..815a4f9ed5 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:2f2bf708bff2bf2a2581c0eb0adfa63b38f0fd5f5fda214594049c2b40a98e09 -size 48731 +oid sha256:76947cf0ea5358646af7726987bd3cd3c49694aceba9850bb0e60a63f3fe669d +size 49542 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 51f05258df..920ac350fe 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:4b372f9fd8570c977cd882f396004fe8fd0794ca3070a11394e0d6af6354987a -size 38154 +oid sha256:bf3add28f315b5f7de06dbdf5b6d5fb96552f648a95a620e50b65fa970de32dd +size 41513 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 new file mode 100644 index 0000000000..a95c48fec0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_5_en.png @@ -0,0 +1,3 @@ +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 8e03ad6ce9..8071ebd4ea 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:bb277b825fefc001450c45a0786afce546f2e0cf7be878ed7bcf401c501c2431 -size 27988 +oid sha256:b4c1e564d2cdb6f6a61a02274d63e2610c9b75f7f53c36b473716bba2400798e +size 24752 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 b37d6fa5d9..5e457ea444 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:c5e6a562525d8ded0fddb365beaef933d2701bcd4548e589bffd54e1bd0a3b11 -size 18087 +oid sha256:3f37ae64424323d6772fca7b43f175e8c02356833bed02bb9661acbfb102c194 +size 19358 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 4592dbb299..6fe0a0be87 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:e22f1f07193011c1db785e3827e73d05d8bf96fd12559e3d7a744b2b978b7bf4 -size 24196 +oid sha256:d42e8795792b250696206f5f9fabcac3b7249b3709cf90d8f2503091769e5343 +size 25324 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 7aea3012b7..ef1431db80 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:de50204e7681fead39e7a8f298149b2657a3663197ddc1cfacce4389641f74f5 -size 48222 +oid sha256:884e301844c610497bf5b5d52445e8124e9eaafd591853a55ab72175096942ab +size 49336 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 76810b5897..6b225ffae0 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:a8a1a8fe51f741748817a0a1c497fc164d0e1e711ce458182a654b1fee8e5741 -size 36417 +oid sha256:81be06688e135d746d30140e61aa168f0d7bfecad1a12e54d9d26e3eb55be0be +size 39897 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 new file mode 100644 index 0000000000..8e03ad6ce9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_5_en.png @@ -0,0 +1,3 @@ +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_8_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_8_en.png index aef669fae0..d0d02d6321 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:781bf8c86e88c9ee564f45de7446ee2eb554efe68ae9e708aa1b5bde7fce6d6b -size 34137 +oid sha256:65446447e07a1a6cc08ed566e1aaea7a1209f1e1f5f752dec5e598819333761d +size 34595 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 3c90a4f7ef..844a7c3c54 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:45c706bb63ab7df8e66898ec67fabbf8c227cac421b8c5eda84a2f6532c8f199 -size 32504 +oid sha256:e467732ff44f080bb1a698d4f6f2427480c5456980066ffd9ab6970b093c3d64 +size 33206 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png index 3639cb1312..59ce066fe9 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c1298d7a7bef09a72be97f859448a877594088482de6e1fc2660e1a5dcb0869 -size 31354 +oid sha256:e40914567317a9cbecd3638bf23cba6ffbaa9be24733909aa58cb8d84a64c463 +size 31169 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png index 841dd9f1bc..8315edcfd1 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:621db291024ceaa9bba2bc3e71a68b6fc67dab8055ede0595f0519499324b483 -size 30644 +oid sha256:19e65122fd39fbf25050c9e982285fea34c4fec06647d713c8ea78c2c813ea5a +size 30478 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en.png index 3639cb1312..59ce066fe9 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c1298d7a7bef09a72be97f859448a877594088482de6e1fc2660e1a5dcb0869 -size 31354 +oid sha256:e40914567317a9cbecd3638bf23cba6ffbaa9be24733909aa58cb8d84a64c463 +size 31169 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en.png index 841dd9f1bc..8315edcfd1 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:621db291024ceaa9bba2bc3e71a68b6fc67dab8055ede0595f0519499324b483 -size 30644 +oid sha256:19e65122fd39fbf25050c9e982285fea34c4fec06647d713c8ea78c2c813ea5a +size 30478 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 new file mode 100644 index 0000000000..b96321378c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_DmAvatarsRtl_Avatars_en.png @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000000..097602787d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_DmAvatars_Avatars_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69e9858a04efe0160908bd0b85990dce8e983db33408ca58752bd4dc1eef6861 +size 13576 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en.png index 264ff76564..1cc489f067 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0672e8d71e04366d0fd7622b3758e7ca310b627b7424639fef58f91c324c3598 -size 46781 +oid sha256:e5bd87fa02dda072996cb8b1c927bceca0a7dac0c790e82681b4c1e411523365 +size 32310 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 28e63caa03..3aa2f3157d 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:d372726c65f1887a8eba63f04301212780ef04410fd0758d314562f6899859ea -size 46072 +oid sha256:1a986526c80130e11a0ec9c6450e4cd77dc40ef4a09082a1c526eebabebf4c03 +size 49746 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 1cd0c138e2..1ef0d7dd02 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:ea6ea46123a211b87daadf29aa87588f4e0fa5e5c25ddc0307db490c72f865d5 -size 96925 +oid sha256:d2ca690a89bebaa351990cab34155afcfa7a0b9f02312757fcb933ba97ab9310 +size 101952 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 1c57c30014..c7e3599c58 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:fbbe069eaeff3c1bfb39e4b3e3356e92864f10a57ca4ccb2029e38b5977b6f45 -size 25648 +oid sha256:bb4d6bfb9c412de00a2b4956032dd42906b5451eb99e6ebb1880dc01f6b55af5 +size 26077 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 e8d101b4b7..c7e3599c58 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97e6bbc6cc6d5bb90606bc7f934258c8060d7ca5fade8da6a1aae288e8558bb4 -size 38390 +oid sha256:bb4d6bfb9c412de00a2b4956032dd42906b5451eb99e6ebb1880dc01f6b55af5 +size 26077 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_2_en.png new file mode 100644 index 0000000000..d7ff4a1d2f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26bf76ccdb56d042422553f557d91d0f26d874a710f696ac106c5c2b5590d332 +size 38833 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png index 0a5082edbe..fe44b8941c 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:2b14f8a5a50deb5dd4854665cd41511228f5ba47b0719bf35b4d1675d0f20ce1 -size 24527 +oid sha256:b8c422787b67d477d3b7c8d5dee8879f33d47153dc93dd29bb3883e4ed863a41 +size 25232 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 6d872bf044..fe44b8941c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:841042eabcd85626a5182108edae46b4514f84a8f22c57e324c02430536acff2 -size 36738 +oid sha256:b8c422787b67d477d3b7c8d5dee8879f33d47153dc93dd29bb3883e4ed863a41 +size 25232 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_2_en.png new file mode 100644 index 0000000000..f5ff7856b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8413aed02383572cfe8c481c6ba8b0db4cfb3402334c37f2d8b54a73fe4bf594 +size 37343 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeaderPlaceholder_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeaderPlaceholder_Day_0_en.png new file mode 100644 index 0000000000..3a7abad03f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeaderPlaceholder_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b494ecdb4962772dd548339a4ac57be40b273b513697ed6d039d1c905617d54f +size 4987 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeaderPlaceholder_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeaderPlaceholder_Night_0_en.png new file mode 100644 index 0000000000..17d1ff9d1b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeaderPlaceholder_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0618a9f769e15b4e682d763224cc1fe0abf62c58f3b9a6b4059153f8805671e +size 4740 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 03d5ad5880..4c4d183956 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:0dfea70e781debf7a293aee2967c3da314101020698fee97d3551e25f5f378d0 -size 10655 +oid sha256:5ccaa7f88a9e46bdc526bfe3d5c2163bf3d963c0661179db97f62559edfd3189 +size 11042 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 3a1ad156a2..0edaae43b6 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:33482ad6e5033800e911dab44d4ba5bec15cfa58749102c5da4e2c57a8e760e8 -size 9783 +oid sha256:f37e1587ba12f9b6326b5b7398982fc663ca913da8c0ee83dfbd5e9decbd4362 +size 10906 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 27f51b2778..741a708fe7 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:62e127900d00756ecc806eb9d8efb6a11cd9cdcecae6b4bab643b7306a06be2b -size 10435 +oid sha256:bcb0063babe7091368af6b5bc7e8929c54ea879bd78043d9128db2dcea9d79fa +size 11191 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 81d4ceda60..c4e8dfdd29 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:e39bc143f7ee45c47ff57bfb53a773d7df2ba14568a9368643d8bab9dbbf4955 -size 9675 +oid sha256:f34e63a88464ddc817d1ffe0324352199ae1821dfa846cceac129a656ece2eb6 +size 10911 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 93e17f26b8..78e3e7db33 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:4c37d48eb1fde1eb67cb3130505414c97486d1b40b49be9a6c0770b0ff9e50da -size 9277 +oid sha256:9aa5b37b57d1ef1f219e0c5213eab3eba3222bd0453988a5c708e15ba89d8fd4 +size 9772 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 5174fbe9bf..10d4beffb9 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:00a060c5b66ee6febd8bfe145548aa3b2fa43ed0852e4224ab08a4641cfdc6b5 -size 8075 +oid sha256:6a992addeb8525c7df4be04514c82e22c8c1269d7c14aa25497efaac50e844a8 +size 9410 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 d55e71d415..af7347092d 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:4e283d48e8eb569f44576c158096ef0672146392411e2e768350ebaff9f71ee7 -size 9073 +oid sha256:e850f65631de21c8cb44965f66e8ffbf0b1eecf2ed6f91e6669f0dc89ec0e4b3 +size 9784 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 fa48ce852e..38dc8e36ba 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:d23f245942d6353707c4252a05935b9d59a5a8ef380f351d0341b804413969ca -size 8101 +oid sha256:07b7d5515f0ca6e6634e8ff4b53c73f48b1170fe8d2488c7a8863d1568c52cdf +size 9420 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 6fc398e5ba..13b54d61f4 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:2a6797d1d45c1ef189dfffec805604b06dd7820976c834233a99fc803721b663 -size 6500 +oid sha256:ca4b2ddcebb957944522673d20aafb967ef118ac1a8564f33a0c8745f0f7caf5 +size 6268 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 b599a59788..30475df2c4 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:acc4553e995907ecfaaf74c4e7bfdd9fd8ccf5cd6ba8396b25c110211c21975e -size 6614 +oid sha256:d56cc64425872a2dc1c12ab7d2e0d9fe43e89586e6f5f1805684350281baa6b8 +size 6744 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 1d334393b1..81b127dc0b 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:2f93df7decca6ced4efe9f739d6f877b2411be874c0cf011a146a66e0ee2fb69 -size 7790 +oid sha256:160abf1659329b6ddb61e9f3bbe9cc5cf4630242429093a8d2d579e0b6efd78d +size 7687 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 efe44c5f81..7ad44c9fd2 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:7a52a67692a0fefcb499af2f3e0670b663b1df657ecc7b61e58773db9ea6615f -size 8057 +oid sha256:39817cc9c0bcb7ae058201c8bb9e7f68529142161019a23296087cd952667473 +size 8143 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 230de5c2ef..08ebefd43b 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:b095f97e5ac7279fba15f7ea8dc1658c89e28e4f72b8f93b7c32a7b6d5bc4d23 -size 7807 +oid sha256:40efbe8c661131800679fcdf18870d05cbd24a7f62c7c1adb998f28cef3896ea +size 7698 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 d45e2d4b35..b7d94eec19 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:5a0960b2353e902b04682818cf598fad7ff4ccee9e9fe7aec13ad556653b702c -size 8091 +oid sha256:774bf98c634fef1339d95e8eaf5f5219535766f41f2ee8b4c124107f381bf052 +size 8167 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 82f7e36af6..25dae1f86b 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:aa985c9b0211cb12e617e8da73669777cf3a5ec5b77b79f232ccdc6e0cf55373 -size 11760 +oid sha256:7ad5934da41af5f5f350adc3d5d754f1ed16e90546cb20a0874e3d069010167d +size 10117 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 e38dfee86a..491523e822 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:71bf90e818949065c7620bfa8e7148a8f9be295775e1b29cb251d2a00bd87853 -size 12173 +oid sha256:81de93c5c71bc1552dc93343bae9168ce35e0d4d4430a47311ff80b9cec6c3b6 +size 10633 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 e3c16b4b40..7201e63c00 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:68e990772361fd240fb20b664731613b5a2f187499f2db48e130673f16acb33b -size 11575 +oid sha256:1ec4038e9d1cd25d8ee27c54e10f9f3627bf6f8f4c83201b1bbc3fd73641f1d5 +size 10333 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 f4bcc684a7..308066382b 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:05e2b62d4484990cdd837ff233a654cd6aa8d82d1c3965351bba86bcfcc1e1d0 -size 11776 +oid sha256:f2a837de06ec9369c5a39629579c237b4f394a586a2ce85a5c5e15d444c98314 +size 10323 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 5d59b52717..1e4ce1ae37 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:8a2d52c8f8903881357cd0590d5c1bfb85997068eded3813aad1bec4f31d853a -size 52452 +oid sha256:86d4aaa9a2ad499be90ed3a0d9a15fcd23cba4b40395aee53028168717c1fe9f +size 54739 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en.png index 355afd8b64..9b60b2ece6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2222b0d2d1589df67d73aad8eee9a1f30efb1392e0c5493f47f69cac01c8710b -size 31044 +oid sha256:08a9c400585956485c4a18bafa78ba4fa8eee8b98fce657077777686871041b5 +size 31010 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en.png deleted file mode 100644 index 851f4f8125..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:20fa72623728a8f1b8206af75dcba3b83384aeab2977fde952bc4d3000aa7d7b -size 43579 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en.png index e1cf8ac4eb..d913afbcbd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ada067f89b469fccaaf4b9751c0469a9f776ef6206d5ebbb68b7b2128d77ce3c -size 29564 +oid sha256:6e7825e195725479409a23b626771fe207debec55953c0869e5dd9bcae210dc7 +size 29529 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en.png deleted file mode 100644 index 24793ec7c9..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f717f0e4ea67364e505e8c756931d11bdb696d7be5f11bb155f085c9f2a668cc -size 42149 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..595aae659f 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:684f94bece9c9cc117eef1cdad6fca99bb085158454a890592254f27c57e9b0e +size 39715 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 deleted file mode 100644 index 8a527331d9..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:67be1e8e93d0c10366f4b37f14ca0f90c1f4ccd5da9024c9a396d3d46507e18c -size 40339 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 deleted file mode 100644 index d53b0ecf7e..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f89a1039bc2d101af319c0b6259d8eebc9d313ae34947aedee525945be23799 -size 44604 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 deleted file mode 100644 index 69da701004..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6144f221b9d11c70d15b54321bfbff3d1de1454e6c73be34d7b2e82bd1625a94 -size 30701 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..847d2f35db 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:881a6235b66bd13269103bcc7070e389041652da49ecdba68c6caf151301b6c0 +size 38428 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 deleted file mode 100644 index e16a2bc691..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a512016e96b933f6089e60b634fa77f140dfa7de50d3a8c1a62092ad552d7f2d -size 39216 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 deleted file mode 100644 index e36f282ba2..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8608e63ef9ff79fd698c202d60a8c73f8874f8610601a0ff6dc85663ed82c7da -size 43537 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 deleted file mode 100644 index 2534bd5d0c..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2898f00a828a69ce9cdd56994d7878f6aae8c5b2ea5d1150df57aa2ebd7e537b -size 29233 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..acc977cd8d 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:9dd2d35e1c0a3b3a9576e35b8a5ad4a3d08441b8257bb48edd0a0816c9f24c68 +size 39634 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..47027cdf92 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:fa2ca41cebe2e37843f74319a56f3c18103929f06765f0950cd1945440909e63 +size 38223 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 3c218ccbbc..8ca8df6e8e 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:b918ce7162d95c873f0029a657ee07fca2e34926e4c3cdb39eaeb10123a08721 -size 25340 +oid sha256:4c7dea18de5eabe820fe8670cd09d7b68160a97f31e4f1c474c790d829854ef7 +size 25291 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 37ecdb10ac..e7f7345c31 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:478c8ee2d55bb2a9f99b8d83c6e0eb0b316237ff1a60a6a735b5eb06f7ca083e -size 23029 +oid sha256:98b39ded83aac1bb5befba6749c08a328b6855bcbd491c1a2f669c848ce72c31 +size 22982 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 e6925f9f5b..086f166d79 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:d62cf7beeba92ee47193a871becad07f50d27e6f6fa0b85f6c620c3f1b04b6ce -size 24697 +oid sha256:0f7aa184c28a4281a30a443b208bf3f64d9a0f85ad135ef50c999d40362c67d4 +size 24670 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 e62cdf3ec4..86ab39550c 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:d684c9ed8b2c38cdf7017194ca656ca8da161ab0a40ef3270c6e3988d8e7f144 -size 22664 +oid sha256:1e13b61fcb56fbd64064bc35628957cb68507efb7fdc25280d6dbf1e6477addc +size 22637 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 ad12fe8923..67fbefb086 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:2b55fce1bf0d6b764aaed8eed0fbc1416c5aa1c5b6160775a641dfa10addb227 -size 8231 +oid sha256:dbd0574dba895e157ff5b69ab23f3b389d280538810b457d2860ae5d77331705 +size 8256 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 db6b026090..6b140844b0 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:4ca666990cb421602e21be0dbf4ec075e8d3e94f9835464923bd31d2414d9d57 -size 7594 +oid sha256:7e87c0990dcaf2511a2a0573a04bf14c6a9a7d24e0291fed18aa3a9ea28da572 +size 7543 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 e40d0af8b2..de0bd78c7d 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:01726f688e5149a460cb1004dff0c7700302a3fe7179829603625dd369b20e78 -size 7659 +oid sha256:1ed14962b63afa972c68a53d2366b9f00906bab2f3436220bb28643fcb1d56cb +size 7668 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 65f3240329..bded2caa96 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:8c30a496415f6c00f5ce3a01c775c78994992efd68b2548ecbb5b5ffac054cc1 -size 8041 +oid sha256:a50093fb47b3b17a2ccd1aeb2a10fb6b5d368add8f7d458e50f0cc8643128b7c +size 8053 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 039388acdc..7030c0a8c4 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:10f58df934f372a3b8c68c8e0c4ef61e5bfef242d65daa8f34f4999249bf9e8d -size 7799 +oid sha256:a45336123e1eec5ea4b21c6932c1c11d6024685f736454e585a0fb6e6a85ccb2 +size 7701 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 f9f1b8195d..41449d52b8 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:6d452b48ef65df5257eb708553d667bee46b9d83b1c29a07119c54e342cf1bc2 -size 7527 +oid sha256:174f9f97fbce115d36fc5124d3301bd148444acdf24bdc933b5a3fb0754c883d +size 7545 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 12f6ba7c36..9b62306041 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:f10ca2d461e4078e46455e007eca2d1e9c9a20dbb6bc24c681fa5164e5f50efd -size 13531 +oid sha256:ed8342b56d749db4d862aaa82d5d312089494965c2879adf18e8fd0c94f8525f +size 13476 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 6273b591af..72bd9cfb6c 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:da386980ce6a45e102727715b5d50f219e39dcb40a513abdfaab9fa725b8ea41 -size 13260 +oid sha256:04a62974dd81e0786127501b8778f9d302db38a27c984d1b403884942d43accd +size 13209 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en.png index 9c0c4b40f2..60c5e812ba 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64d834437c7049ec9d81331b382c4bf0cdbc603de8e403e0addf6a4947e815a6 -size 700141 +oid sha256:9e514d7f36fe25150d66c2d2092982a696196a5a0a2674eef97b7231c0c03bef +size 699410 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en.png index bcbfc91e12..5a96554c0a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:398c2908d73fb30117ce917b25e930c578eaf2da5e0c41e126f262f129bda8cc -size 699710 +oid sha256:712af2723a0656ffe927f1cd488d3117f5b450fa405dad307b0c88a9f2483f9c +size 698704 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en.png index 41e45ad002..e4424188e6 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:8d6ab1331e65a4d02accb4d20c899a06c37a022d9f9080133799e30763969300 +size 26774 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en.png index 99f5bb1dae..3dccb4c068 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89da32776c436ca00045afb76f42c5f0e5c59f6d682d59a3204461b10ea95474 -size 26617 +oid sha256:010120cbcbfed1e7bfa3fb4c88df6e1098d0382cb1834f1a8046e312434f201d +size 26549 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en.png index 9ca317cf9d..d0139ec184 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:d265f0c3fe5bc7f5a6e6d6cfa022ba16ca770b65eeb9a742ba250ff55c4b066a -size 207625 +oid sha256:52174b55b1737787260a454c59f243b0e3f6327ef5ec71464744def928d165d4 +size 206785 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png index a737f60d1b..a32a45029c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:348a194ad17a0a0dffda47c7387edde99436d543e9bff1726608946e92558830 -size 185553 +oid sha256:99478fe3d9d44e9cda39f33748c70017203a2cce5cb855605740a29360221bd5 +size 184910 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_17_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_17_en.png index 2e580e55a4..a855818ac6 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:4e8ce597c240a7e72b6811537b6fd24e1bd38714db0ce074377ab3f16eaf0436 -size 656958 +oid sha256:062f8342a3a3715498ca34488c2d9ffe09c1e6dbbe04df09e88fd107a33b174a +size 653228 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 deleted file mode 100644 index b18a209110..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_18_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:46d390cfe8d41536cea5e90cb38aa547969ae24d82027c02cfb71c4fbc780247 -size 667948 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 deleted file mode 100644 index e22d5b8e72..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_19_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0bd4f0133fc3a4a159e3eda1f715e4140464b7c67558c551216546d4688f21bc -size 669143 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png index 312b19a389..accbd26985 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b23ef4fb29f51e308b74681017fcee257308edbd1e218e4daf5736501c70e0de -size 699637 +oid sha256:3effa629295c12d2248924cf298599f3bd28975ecc76fac84f771c16266d738e +size 698895 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 deleted file mode 100644 index 1023508292..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_20_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:178de2176a3932665897486ecbee622083af2d5939f4f1f7f0bdc4667a61e36a -size 668061 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 deleted file mode 100644 index 9fa3e43792..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_21_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc974dfc6206f389153477d287a69c401ddc154a529cb892227a743acee8ac50 -size 668212 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 deleted file mode 100644 index 081ab9a80b..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_22_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e7ac632f2062aa4a13ee46fd2fef6d9e6be25270f4790247131231163670cbe6 -size 670668 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en.png index 88e1c15416..b9dabc97ee 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af91214346ed087a8cf936b9ddc5b78e8840a48f6dab6b856ad79abb5fef1ea0 -size 252778 +oid sha256:1fd3b7065860d1e51fe51a0a6e3f4a9e61a77ea91cefe85cc3aa3560c4cc6bb2 +size 252253 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_3_en.png index d122817b15..f11d4fc3fc 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:21b8289db279172f042fa902bdbdb70d87104d342a1140357bb1ac2bc18f6590 -size 668482 +oid sha256:7f9ee78e2a034f34d77a438d305a5cc63ea583df80f83a14b1d81fcf74ca93f4 +size 665416 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_4_en.png index 797c1cb2cb..5e081d3c4e 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:f72964447f2fa66e8df38fcff4a84ada1cf62e1646a2e8a00c61f8f151abfa8c -size 206507 +oid sha256:d1e91c66363a5af74d4345f7837db178410c078e390b0cf9296d0ba4b3dfd7cb +size 207004 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en.png index 8311ff7878..d61e0a50b9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fcaacd813136d9cb22d0542480fcef2be05cae7ec44c0c3683874086e1c7a4b -size 184107 +oid sha256:9eb950f3c0d34a796ebd7635ff736023742d5e6a243912f3ad2234ecf08694b2 +size 183220 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png index a092bf37ac..f2352c88f8 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:7f6c84fbd21949a3d9d355bfb627b8a70f65fc669cb6b6818a325fe4577351f9 +size 196092 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en.png index 619bd4e8c6..bd37e3eb79 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:26bd927acc578cb3585f6e101c066f0a6adb6a6e424dcd196388d5a838d8b22b +size 196396 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png index 77e9dad534..69515d4326 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:094c1de97c33431390f3ff71d73019d9af9a62ed57ba98c94e53f1f9680851fa -size 210728 +oid sha256:2c4440f5ed01f2c1b405bb2d444c52455ea8ee1c094baf7d2f85bd9a9ff98b02 +size 210117 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_9_en.png index c3caba08ff..5d254988c9 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:2a17d0f4bc47305b9c8a3866f70cdddb16fcb5ba10c8007c8739a329025e846a -size 211342 +oid sha256:11f140569061e21e6b546118b57340a0830779580498a31627acb2e6c7313471 +size 210490 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_0_en.png index 0b90eca670..03f85f5233 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f17669c1a3a65bafa5786ac23245117b6956d154a4ddea2e95de9070a5bff07e -size 390004 +oid sha256:2b5af8a5aae566e72e0223d40a5978ab191911c68739e417503d8fd5058f19e7 +size 389408 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_10_en.png index db9cb01f26..7aa5cf885d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bd5c7a42aebb6c2ebce9a529bafb23145e5a3a1d10270385386ecde9ca03c19 -size 389486 +oid sha256:aec4a5e70320024eb053c56445c226758818550745f2d2ddfec16721e43b6d90 +size 388674 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..47027cdf92 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:fa2ca41cebe2e37843f74319a56f3c18103929f06765f0950cd1945440909e63 +size 38223 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_12_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_12_en.png index c38069d18e..4094507bdd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_12_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c9c5240346788914d7ab6a825c38f6fc2d6dba8bdc2879c2657cdafe3718b34 -size 31511 +oid sha256:1e53770850ee2e0d011ed91360a2c02abf80b27cc2cee66699331684637d469a +size 30960 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 4734e4e6db..89b6cfbfba 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:b593f6827598052cbde0e580dcd43b92fb098f6755779b95a60bb691d9ad5003 -size 134711 +oid sha256:9c7c09c4ff8e66c23d3cdf5c79ffe2d3305d569e3aa1e239dfbb981537e467cd +size 133920 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_16_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_16_en.png index 7ff9c8a971..9836fe106f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_16_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:730cffc57e88440ec4607238b383e68aada1907e6f9829a3bdc4bf8a467c1788 -size 114080 +oid sha256:df718693c63e4de07d17486a80103240f8f2d56a21aaf1a2b18a9237f105364d +size 113630 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 be398e06bf..dc06b5afc9 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:2adfc04e0f7999ba861bf931b853f068067387558e81464e99caba1c2445a3a7 -size 444811 +oid sha256:8e353d24c2b9b49abaa11e064b1581215ad65154d0e67372fe24684ba2695a0e +size 442063 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 deleted file mode 100644 index 057e117409..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_18_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0487a6dbc3a2f3bfb79a709c782fb692ef93f37607b4483095ce76f08954580e -size 396582 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 deleted file mode 100644 index bc7a7cc257..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_19_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:29833e4103ecf4bd3e1c5da384d3bcc20031073a14261971825e97bc9f1dad23 -size 397593 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_1_en.png index aa35a1751a..5b86c94b5b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d8a4b42d3857ddaa1ea86d4ab3a872c5fa0014061b2a116895616bee25424d1 -size 390037 +oid sha256:d0ed6080a007c98867a5c4f9b32570ab3042b34030b5210867110374064ee8fc +size 389436 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 deleted file mode 100644 index aa8d934627..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_20_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b544362587868c37edc424b675c6758e7ffcff359fa12c1cfcb874a0502e25de -size 397284 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 deleted file mode 100644 index 26758e047f..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_21_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:51e183ca6d518d803471ad4eed0ee932397c32a4c1ffb064714b39f13ea2003f -size 396825 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 deleted file mode 100644 index 7f34e6d53c..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_22_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7b6623e0e784a93d6a3aa220b620efe7d1f9e040aedac7f34d01b24262fe101b -size 401770 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_2_en.png index 5663001649..eb7fa67df4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c4b715349adcfdf5c3cdd95844846170b43dc191801156b2e822f0eaeb92033 -size 95392 +oid sha256:4feb42b25bd952a0305ad2b1c4cab9934586693d804336e1ba23e9b247f712c0 +size 94961 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 7d1ba874a3..f55ed35a60 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:d56f08f1c37575de90a67ebfeadaf1c891c9ac9e4925af7d048922df9fec5aa9 -size 396815 +oid sha256:eaf4fb4908967c6092de9be0d7ca33108f0f507605d7591395a08594763fb0fe +size 396202 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 52f17795bb..8a41530aee 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:e3a148746b4f7428688a634f2b1299f0393bf669a665c94fa9af12d7839e72a0 -size 130834 +oid sha256:bb6999796f9275adce6b5e05b362649cb738c759af649f7bbb9c0ff01548a579 +size 131776 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png index 56e01cbc01..fbddf16dfd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3aef5ebd6889b0fc8345db6aefeb6d2cc26a5a2349632dac2e258caa87c28b53 -size 112850 +oid sha256:04ba5da6b618ae3abf4ca06b4ae1baa13e46673b0be73ada2b15cefd79063122 +size 112146 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..f13a902fe0 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:8273abc2f10e6c42a65fb6aef2e237d58b6e4d858df1f35be255b7030fb63c44 +size 123522 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..ce0848b5b3 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:456dc57223044ece1f4136dc01765e41a0b649c827ad6e6b08370e53a7015dbb +size 14378 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 932ddfb632..06e1c13225 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:43ab4873fc5fc812bf18af50cbe620c83b273ee70305b5ea06a7aeabdf8dbc93 -size 137637 +oid sha256:b5fdeb06fb15486d2d4949a38da2f6eed5ef94a0386e9daaa83a6b00bec9e392 +size 137096 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 924e24ba8a..dfe63cccae 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:33048dafab286cf0e5c84040c034d93a42d69c234edd53132d918a4a1c135ece -size 137908 +oid sha256:9b64a6f9fe548897b6b34961bc077718265acdb8f894d5d2d697d5409c633368 +size 137247 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components.markdown_MarkdownTextInput_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components.markdown_MarkdownTextInput_Day_0_en.png index 678b74ef59..8808b06521 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components.markdown_MarkdownTextInput_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components.markdown_MarkdownTextInput_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22ffc77f78c6453a8719f8112fbf9bc97b41bbf9b738f701764599b4b8aa50e2 -size 6342 +oid sha256:37903c21d648cbc2e761b32bc822bbe4099b8259e78e3a5b75f5677f2979c20e +size 6208 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components.markdown_MarkdownTextInput_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components.markdown_MarkdownTextInput_Night_0_en.png index 7172c3fee2..42b189e5be 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components.markdown_MarkdownTextInput_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components.markdown_MarkdownTextInput_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a5b20dcdcda35f57296a96cf44ae67a0e9a7798a073eca0d49380d93e256606 -size 6133 +oid sha256:17b8d1408828a857eda20d194c342bef45151a141d294ba8df756d9b860e6d98 +size 6008 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanThemeInTimeline_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanThemeInTimeline_Day_0_en.png index 3acdc88bea..627828a287 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanThemeInTimeline_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanThemeInTimeline_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9e68f504334bf51de555c27936d2561f3def8c7920189512594b74ba9770105 -size 35720 +oid sha256:8e171ced0a7f9994d1e9addb093959fb455727d81912aecb377742541181535a +size 35733 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanThemeInTimeline_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanThemeInTimeline_Night_0_en.png index ed7123fb14..ee87ced6f5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanThemeInTimeline_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanThemeInTimeline_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7523890541aefd0f65533247e0bb0b5c306946b19ab7d27cc8ff0d6e69e85478 -size 34166 +oid sha256:ba17ec26a4807e2cad64769b98140e2d77f133e39be72090a1f44ca143427833 +size 34106 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanTheme_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanTheme_Day_0_en.png index aaabcd8f67..140398dcaa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanTheme_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanTheme_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc5c6f3dc41efa8c969c70ad36c1c4ada0feaf279767d0e2165a5837f568e2e7 -size 49441 +oid sha256:8366d3c9ea45d6b7e24184b5ba9756cfcfe8a592ec19b107be1168b307840192 +size 49433 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanTheme_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanTheme_Night_0_en.png index 508c21c942..0c0a38fbd5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanTheme_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.mentions_MentionSpanTheme_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d091d25da896e86e0851cd3d440f7b4b05dfaac6240d9639b06e9e362c20f5ca -size 47304 +oid sha256:79cc95b3838f24e85a87d5f0116575ad74e2abc03b77a28464d7fa82fb357840 +size 47343 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png index 4caec79857..1a6f5d660b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5aa13d38c3c677dc552e1ad6ffdc19caa37c96287945b877804584e418e4acbc -size 10915 +oid sha256:30ae0b86a9834a90990a42f6bad0fc9010b4109bfbbe85072f00d61b0664e6e2 +size 10926 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png index 6482d7322b..1c65fadbf4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:416b0ee79cd8c07fed42739309059895f8c36c20d3838be4a5a4afbfbd0b5394 -size 18548 +oid sha256:d6c87eb2b2da4afdadd949bd39e88389c34053adb19d34f8958f97ae1faf90da +size 18557 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png index 6f42ac691e..42f4e1a941 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c82fdba61809fce17e2eb7e9a79857a260e5598a2a9db88f481db69e2c7f79fa -size 7745 +oid sha256:3c49dbe21e9c465adc1f12a57b87dcb06569936e80d047e0db77314cefce4691 +size 7756 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png index fddd092e48..41e20b4266 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36aad99f720579ad309c01591f2d4e765925aefd60bec2f66a28c14d5748b855 -size 10551 +oid sha256:7523f982565c692f9986d11bba14ecc51a3401700de5a752fe5d2b667d0ea236 +size 10560 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png index 92b79113e1..be3a01d3d2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d527056f8cc0ae149aed408898cfc47cfdaba91cc7367e4cbf10fa49df44f9b -size 17781 +oid sha256:73769f2c3ced2e20f6a3a91cd8cd040c14e93a19c923d5784e2081cf4baf2d44 +size 17790 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png index f59422b33e..90d85686d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9042bb2175fb555c005faa1f69bffd1640c91a3c109a18d4759d802d1540350 -size 7485 +oid sha256:ef3e1d6d11520f6475059032d53c9c0a6e80c133318a0ae44dfc67d9b69c31c1 +size 7494 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..37f723e839 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:b0bcb9f9715c5ecf3007c2128f68e040512645c5db10db1adf6cd2bf118d50b5 +size 50279 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..02237c1549 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:02bc44adb506a0a8b71ef96d8f3d5b9ce0ad2ec7dc9cba27eca8e0bfcaf804c0 +size 48113 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..c36a00cd27 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:b170b771205f42a4aaf6e23b1043cc2b7ca591cc940fc729f8c7ca62e1778ccc +size 51327 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..ed3fbcb332 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:db1c5fb588286dd755213c2eadf23488ddaf9eac7e88585a339484511382cd4c +size 49522 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..4d6bf8bbf9 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:008f560c95d92128a7978478563d47bbf57bc0ed1d165f69c60f3d088a8b1798 +size 41741 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..44fd390226 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:1e8f94bfe6ac910599edd6d2a49ed0369e2304dc589cb574cbf095157e2ae507 +size 39148 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..ea61e45a32 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:5de0b9b9e57b29ea1db60e136654359a05373788ed2ced2036fb536ca20d74f5 +size 50358 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..fe3884483c 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:fc2fcfae3ff423b0c805a24f46e42a3321830a160f42e0d82026da1bef6cebcc +size 48490 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..e4cd320317 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:6f26a02838efca6a91ab005822913eb770e9cf06799653532905814f97848567 +size 60599 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..fb52919dd3 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:008f75cc5a2bceeba54406ff74c2e5541ebf7a39c00489afd941cfcc79e6ada9 +size 58085 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..37f723e839 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:b0bcb9f9715c5ecf3007c2128f68e040512645c5db10db1adf6cd2bf118d50b5 +size 50279 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..02237c1549 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:02bc44adb506a0a8b71ef96d8f3d5b9ce0ad2ec7dc9cba27eca8e0bfcaf804c0 +size 48113 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..cf5d4dec00 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:6dbcd0f76ceb070d1a7b3f9a22292b37c5bd9fa005915233db7bcb8f45d4ccef +size 61775 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..d7cb6108b0 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:335853740399624113c6e83d2cbc411691eb74759bcdcc1575b7fa3d10ad4ef6 +size 58751 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..d04df1d38c 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:c8cf7ee8e457e03a2abad0419fb8466f3f43b85abeabf705dd7af412dc4e4d45 +size 51250 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..9b53a68b10 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:a6db26883ef045ff984e8cbf6256a01b8221825efa29b82364187c6b4fcf9d2a +size 48653 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..df3af23261 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08ea9ead35a143a4a14fa6b4185258417466d7d05659ac839c823d80a950a8f6 -size 71689 +oid sha256:2703da4167bd0198f9666c2160bf95e4c99ac718864ae67d277fbbb4fd50014f +size 71876 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png index 892b7a03b0..6d8f6e1a3e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa695595ef8f487bf0503648024f4337ac8b39035a79d8c5612c366dd2c8c570 -size 58177 +oid sha256:86f6e18357569284c223afb87195be656e6e6c90794e64ef6a13346c8de6c496 +size 58367 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png index a2bb96eba8..2704eb3847 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e6e9f5972f3119468855637eb0ff4e975ffe1a36162bd88485ff34598bed6d5 -size 71258 +oid sha256:05f9317281ed84751b9f168c4495e94d1f68a9f9e7946270402db59e259d1ac5 +size 71392 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png index b1ef3a5c64..6cc38e5ad5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36d9ffefb8b575dbdf308b4075511edb2716f1b5d5de7dcb96a0a13dd68d9052 -size 79811 +oid sha256:be4cda10c2b416d70d020c46abb00e41a2802e5d71f2dc416c506cffae147af1 +size 80226 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png index 1e939c2803..a74e2710c8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27320b5230058ae90d35f681eb46a64fcdb0a83037b2e5ec2fce49a331e4f525 -size 61091 +oid sha256:3d29017d9dca92779074b963ba29681bb6cc83728a15ba2f55846c8fb2751d1d +size 61119 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png index f4afd108ed..f3472de5f0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e159e59cc4fbcb7e33b4db5f0b04b486ab7d07426371e288c9bbc2b80c05097c -size 59854 +oid sha256:a8d1701f1be6087e93d15bbeed97659e57e0d58b9d5087fad0253d5d3dd60194 +size 59972 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png index fa92ec10e6..be14ebd9b2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db7a7db0cd732c80d03b561ed3061843bdecd23c6f188a7117d3674a62fa15e7 -size 66757 +oid sha256:d2d984f17ca414c523d6a2cdcf9fc4f82c0759b22b72b60e8f5bb28776350a72 +size 66958 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png index eb02cd57a7..c0ee61b312 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f4d9572d105726c07387f037e65092adcbb8b631d0406eabad30dbc8382383d -size 88343 +oid sha256:eeb25a0b7738b0a8b12a2fbdcfc469dcfc2c893ebfa4a50a3047491f817ff981 +size 88665 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png index d6a380f3a1..05cb3a4a89 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9dd1cb61ef6b1a6f15d766882f6d748ff8b45c4cb45588ebf8e3787cfac269c -size 59158 +oid sha256:f1f008fc8bc035bf9256e19e831691d3ac7c3347e83aaed2ae8d4b7a7f5b4213 +size 59285 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png index 3ccd6de45d..63622b0027 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5286b5cea91c0f474d86413a1808129cecbf4a5b5992dee2059ec0cbe6e74ee -size 59207 +oid sha256:e9294ee360463d72968ef89eeed7ff52c80da29e4a59f36f00422f1fe3cb7d9f +size 59350 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png index b4712967f6..62268cf4f8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5330b8699041f01a5874c92152f8b6524755d327612d976c7b731ac9def233e2 -size 66344 +oid sha256:5dd13cfda2b0f029a46477cbe9f8db1e3a191890072665adda02ec0750cddcd6 +size 66535 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png index b9cc103539..da1080256f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65f9224c7c4556477c362fe81057d3fc89fe9043e447d225809dbb66d1895f74 -size 58644 +oid sha256:9b32f9442e9088eec377f74127c99002aaf2c5668b0550047dd7b1c9e73ad619 +size 58783 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png index aed4467033..9e1047504c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ec9a6e9914fcaece7a5fc79a94437a6798a9ef9fe29da301a9b6b199bc90ecc -size 68290 +oid sha256:24baa66c2fced922b328987b51c1045af7111fb3c7e4bafdc46046eae61d0f87 +size 68773 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png index 9499864bec..70a49794e7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a765b2e132737bb0213be31344fea05e60dc46b30b032c0c53b1a7adae59359c -size 55167 +oid sha256:e343b71b9f170fa7879510544cfc95af1ba15ebc128bc5d78afd49d10c70dc66 +size 55802 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png index e65958a5d4..c04c1cae02 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c26f7171640584447f185263092281a44b25c2cb3cef9e470cd9d10474c1f241 -size 67857 +oid sha256:d76f37cc5bd2d6ee9b7c87bfe7b656b0095ce6ded4d07cdc48d255039aed3520 +size 68313 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png index 533989345d..98a5dfd03c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bf5ef7638d415bedef349848c03bdcc093e00f391d91c5d528b466ee8117a64 -size 76423 +oid sha256:d24998e960d2d4d36c1ef0f589e9094d972203016b0a923268c1894bd09f267f +size 76999 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png index c8636a07f9..ba691c8f65 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc76afb5738bb1cc70ce048a990cb428dce012eebbf503df59439cbd8546dbe4 -size 58006 +oid sha256:62592587ff02f0d36cb40f0b59fa3e7c6925cd42622ac76c4d561f6df999eba9 +size 58404 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png index 838a92aeb7..7ee8d6355c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30b9d933c5e1cf1a5f4bc721cf8328b87ea025fae703c239a81a6d68748775d5 -size 56888 +oid sha256:fc54d34649b067af6e0daa7d2e1c8661572fe0139ea9aed209b28c3a99f44f12 +size 57353 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png index e9afa6fd69..80a0c981fb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5308541d962b0835476e6443a4dd1af53bebd477ab2f382daefda9121483cff5 -size 63637 +oid sha256:10f02c7de0575dc30c65cafa4adeb265d6ea1e487db240f1d4052be138a8b0b0 +size 64014 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png index fe9f9527af..3c4f0dff27 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2325468ef6b2c89b2989d7ce80526cae29eaab3708da4bedcbde0b2c4c2f9dd -size 84835 +oid sha256:a196fe9e7c551fa7cc1145c9f8df12c5905678fae285884b71e7ea15cea1bf5b +size 85088 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png index 139c44ba12..616dacf9a0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:451ef300ac8c65887a1caccf643772dd331f59dd1181c97f0b9c53a0ff457f89 -size 56207 +oid sha256:60b3b04167249a6283dccae1450645853df5e05ac7a31f419c674be0594dd413 +size 56710 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png index 71a9acd0b1..256094a264 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bcfaa8bca539371c914e56ed1108282f0e4df826f99527ce982a720f22d2655 -size 56192 +oid sha256:d303c75fe252ee9d3fb0bd91131ed44b731ea6fbe120dc1379aeac4c4d43c467 +size 56693 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png index c4f985c80e..9aa831dde8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d65864db2aa76e16df454f6772c296ac4cb9efebe2a417560e2b0e8e65503a89 -size 63211 +oid sha256:81d23de4f3afac7993f42ea8030f6a2392768b343761408d6737382492443267 +size 63586 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png index 6c2e72c7ce..3c59d53b44 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:483daeec8b5d0be508de8f979ace30bdda1ee1c7b3ccb33af0938deacb8f0f40 -size 55673 +oid sha256:7c3a1636c58e8243f971f69ea3575e75a12526863f1304a32f65fc2991fd403f +size 56250 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png index 2114fe5594..93c1f0a9ce 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:247d836caf3bed3b89c6a772861d8e302f0b9c5f9c299edd316ad83ce55c8d9e -size 73090 +oid sha256:8eb6049cc99813238995f936022fd9fd5a43d1e917bfc69ab1069fe3da456cb5 +size 73316 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png index bef915901c..fe7b50f2e8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3006fc80bef839cb8a2a1ad83f9572b157f0abfe8fd9f170de2c09f882e02d0 -size 56132 +oid sha256:611527e8c0bce7a9fb48b5eff42170cb9da5f40cf020c78209dee84615d300ba +size 56514 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png index 72e28cbe9d..dd8f8d2a66 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3c5de24b62819c74c8183ea8a934e3e463bd2da4d00641341ade7e58b644e61 -size 71496 +oid sha256:77b28be1b95709082e0302cca5b65c2d57ab48786e6a22facb8c86116f058b11 +size 71667 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png index 5db52bc9c2..be45e720dd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b60c12e4345b74815300f992b5c95a54c9ceb4d91e511f13e98e2f2805bfa1f9 -size 82673 +oid sha256:31875ed4c6d9443418899de47eeb5b61d2246d8321e83579681cfa152ff4a056 +size 83174 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png index f01e79ce79..e7efad21d5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04dca8661539bd8c9a4afd97f1882415d7c27858b2b489de4f857205543be4fc -size 59397 +oid sha256:5cc4f388f677bf04259344b5bfc9033ef6a4a76ad161012abcd0239ef520a8d8 +size 59670 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png index 3146c25a3d..638e2761d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8adec1a157ed614916988f563391dc817319bb27b10d5b4a3a41da531e9554a0 -size 58466 +oid sha256:e57a614424396e36229d7877a0bb222a51e9e955a42e180acc1d43c048684da2 +size 58787 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png index c6fa978c91..6c510542bb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bea8196d7650886772d346a013e85307005d9a3f24b5a681a14fc1f218b971e -size 66295 +oid sha256:4546f32ff1eeae0a923d473491926a6037eddf59c0c88c516c504b9875874c8a +size 66402 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png index ae98e95d46..d472874510 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3e535259e6d81f396970531e095453989908e91f4a8ecaf47325430eb86c955 -size 101506 +oid sha256:acd25c00d9127c2c78f48b22296a5ab33f1e16ec5698c93dd29adc9172467aac +size 101706 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png index ea8fccf53c..48f0ad6881 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc534ed58d1faf6fbf1f4fdace08e02958e60b54c457436eac2a9c1357b39517 -size 57490 +oid sha256:d066e82aba5742c7e84a260de0e4578bb3bc8faac097225271cfdb14cd52f5f0 +size 57816 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png index 171702f3db..f8f0337792 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a56524de1c9f55eb3d6d88342d6d8576508d6145b3fecb8dddc38c598c82a67c -size 57356 +oid sha256:2c037b4de113387ac7e28d256d28ce9308be184b4ebff988ed96760623acfafd +size 57776 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png index 882819289b..6278391831 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c7299aba8e5340b443223858d405f99ab1a4667721b0abbd895713111055116 -size 66570 +oid sha256:81883325e2e450c33f38b3143c43a3fa3d659a210ff6e9ef40de71b1d0522f50 +size 66781 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png index b27cfcbcf3..07c20262cd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4294a5f0c9b943ccf3e9c5ac6f9ecbd574c34cd5334a044342c72cf72f0ef80a -size 56848 +oid sha256:b6b5808bdde620cac3fe980aee1f8162a854965113d99d074442a2baa818d712 +size 57198 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png index 182b04ed36..c41f41d043 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2a3c8af39fa40b72e95cc4191f820ab2b2b173e37b99178ff7c06a5870ca2c7 -size 69971 +oid sha256:7174f340d6e16be525da9a06270f07abc7496fc6c41a5670a71bd1b23f4a72c2 +size 69957 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png index 834036c242..5d3b894ebc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73a1a7607c6273081ef1b866be384fc40b65283cca4f72f701f174d913973c85 -size 52902 +oid sha256:dbb0c22089c063c18042c8778c1b69edb441af8071f7dfb9a3ffdaeab4ba4c71 +size 53340 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png index e35a7cd447..61a6581ec4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5bbbd86cfe40fb6e861a9473f0a63bcbba808d915da99b14f085a27b9428e15 -size 68300 +oid sha256:e9a561ca3aa61371cc1bd0c1b51508469a0343de3bd8f404055041591c60d42f +size 68231 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png index 8ca87c0c40..7c25276223 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9610ae365f1cc089b17cb068e66c5f90414364c6bf181c1c7ea2800353a9914 -size 79614 +oid sha256:c739add59dd40303652319c0b789ec4d0d2224cc097598ef717e8fb101031ad7 +size 79471 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png index 45d11dd4ac..a3fcdfc5f0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:072afde5059b3a90977929811f43402d396ac64debf2cbf17c1f6efd35394b46 -size 56298 +oid sha256:61d30aca9b213938462659c2dcb8bc8ef10b28821f8492715003262f4cae142c +size 56761 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png index 06d1f5c9c3..e10bdbdf26 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:563e94244304f75a1d64151fdc35e35a9eee2f0c88d4dd4e4872a580adfc434b -size 55392 +oid sha256:a84be2101e96ab4e7566e4fe1af14243466c7dba7e540fb7502acbbf70d33aaa +size 55835 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png index b10f296866..29ac8cd6f3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:529ee850c430ea3bd54aac5b0a768cf49dbce44c6629e41ca4a8293e60d993f9 -size 63140 +oid sha256:9f168b3af0e3679a2d7a5f2baddc1523c1afe9dc433d25c267572097d9ab7d15 +size 63284 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png index 80fb5cec31..f405aa34cb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38ab31d94f2252fc5a6bfa720529eaa1d0f9459395e77383cf55b06547ca6117 -size 97622 +oid sha256:41799e15fad757a976b278d5d722fc755dcfdda3ad0880a1f4058a337b96d470 +size 97907 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png index a83053dfd8..9b72a8c4fd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2b367cb2f3d35dce7c038c3358906e32669245537df0e4d6ec33c21ce6c23ff -size 54362 +oid sha256:c3d27473a9375edc590aeedf8dcb188e1c23785a8f8c62900aa439c33f86ca13 +size 54772 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png index 478b23ef50..7a87bb30d2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57a1fbbce28ef24a5190758b2482622fafda8ec6206b68b8ed632e0e946dda87 -size 54131 +oid sha256:52a125268370bb369c53271f204b5f942da75f01639a39ae6b935d3f3ab4ef00 +size 54705 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png index 93ddb25b2b..d0e908caff 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:261747f96fbf93367dae4819a2d4ae8883573cc75af1d6f43cae77f0eaf55736 -size 63307 +oid sha256:56d531df665ae3baa6400b0fc83b1fe756ec0f452ddd2423457ed26ea18aed98 +size 63628 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png index d142023a66..29df858167 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5786c4a4e98207f229f41a32defb71e059ac3ea9380acacd8883e188c68e81c -size 53735 +oid sha256:d4e66a9e00b7aa05294522055cf6a89e68d409496c1a24ef0b6d841221f6883b +size 54122 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerScaledDensityWithReply_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerScaledDensityWithReply_en.png deleted file mode 100644 index 3806b4217c..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerScaledDensityWithReply_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d35ab62460942f4f14edc87aab84f4d659b18763c4cf8a1c353f99085c1c164d -size 16727 diff --git a/tools/adb/callLinkCustomScheme.sh b/tools/adb/callLinkCustomScheme.sh new file mode 100755 index 0000000000..7e6c9f02d3 --- /dev/null +++ b/tools/adb/callLinkCustomScheme.sh @@ -0,0 +1,14 @@ +#! /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 new file mode 100755 index 0000000000..43f427f22f --- /dev/null +++ b/tools/adb/callLinkCustomScheme2.sh @@ -0,0 +1,14 @@ +#! /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/localazy/config.json b/tools/localazy/config.json index 9936341cee..b38268e5f7 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -163,9 +163,7 @@ "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_device_code_.*", - "screen_qr_code_login_verify_code_loading" + "screen_qr_code_login_no_camera_permission_state_.*" ] }, { @@ -221,7 +219,6 @@ "screen_notification_settings_mentions_only_disclaimer", "screen_room_change_.*", "screen_room_roles_.*", - "screen_roomlist_mark_as_.*", "screen\\.edit_room_address\\..*", "screen\\.security_and_privacy\\..*" ] @@ -273,7 +270,6 @@ "screen_room_timeline.*", "screen\\.room_timeline.*", "screen_room_typing.*", - "screen\\.image_edition\\..*", "screen\\.media_upload.*" ] }, @@ -333,7 +329,6 @@ "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..2012af8741 100755 --- a/tools/sdk/build-rust-sdk +++ b/tools/sdk/build-rust-sdk @@ -41,14 +41,7 @@ sdkArg="" ## Argument parsing -# Use GNU getopt (required for --long support on macOS) -if [[ "$OSTYPE" == "darwin"* ]]; then - GNU_GETOPT="$(brew --prefix gnu-getopt)/bin/getopt" -else - GNU_GETOPT="getopt" -fi - -TEMP=$("$GNU_GETOPT" -o 'rs:b:at:h' --long 'remote,sdk:,branch:,build-app,target-arch:,help' -- "$@") +TEMP=$(getopt -o 'rs:b:at:h' --long 'remote,sdk:,branch:,build-app,target-arch:,help' -- "$@") if [ $? -ne 0 ]; then echo 'Terminating...' >&2 @@ -60,32 +53,32 @@ unset TEMP while true; do case "$1" in - '-r'|'--remote') + 'r'|'--remote') buildLocal=1 shift continue ;; - '-s'|'--sdk') + 's'|'--sdk') sdkArg="$2" shift 2 continue ;; - '-b'|'--branch') + 'b'|'--branch') rustSdkBranch="$2" shift 2 continue ;; - '-a'|'--build-app') + 'a'|'--build-app') buildApp=0 shift continue ;; - '-t'|'--target-arch') + 't'|'--target-arch') target_arch="$2" shift 2 continue ;; - '-h'|'--help') + 'h'|'--help') cat << END SYNOPSIS diff --git a/tools/sdk/update-rustls b/tools/sdk/update-rustls new file mode 100755 index 0000000000..d8ad883d69 --- /dev/null +++ b/tools/sdk/update-rustls @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# 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. + +set -e +set -u + +VERSION=${1:-} +if [ -n "$VERSION" ]; then + PACKAGE=rustls-platform-verifier-android==$VERSION +else + PACKAGE=rustls-platform-verifier-android +fi + +cargo install cargo-download +mkdir -p tmp/rustls-platform-verifier-android +cargo download $PACKAGE > tmp/rustls-platform-verifier-android/rustls-platform-verifier-android.gz +ROOT=$(git rev-parse --show-toplevel) + +cd tmp/rustls-platform-verifier-android + +echo "Extracting rustls-platform-verifier-android.aar from \`rustls-platform-verifier-android.gz\`" + +tar -xzvf rustls-platform-verifier-android.gz &> /dev/null +DIR=$(find . -type d -name "rustls-platform-verifier-android-*") +AAR=$(find $DIR -type f -name "*.aar") +cp $AAR $ROOT/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar +cd $ROOT +rm -r tmp/rustls-platform-verifier-android + +echo "Updated rustls-platform-verifier-android.aar using \`$(basename $AAR)\`" > libraries/matrix/impl/libs/rustls-platform-verifier-android.aar.version +cat libraries/matrix/impl/libs/rustls-platform-verifier-android.aar.version